diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..565db34 --- /dev/null +++ b/BUILD @@ -0,0 +1,4 @@ +load("@bazel_gazelle//:def.bzl", "gazelle") + +# gazelle:prefix github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra +gazelle(name = "gazelle") diff --git a/README.md b/README.md index efc6f65..b9f3a5b 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,29 @@ -# TestRepository - -# branch1 change1 -# Kavitha Branch1 change1 - -#Divya branch11 change1 -#Divya branch11 change2 - -# Kavitha Branch2 change1 -# Kavitha Branch3 change1 -# Kavitha Branch3 change2 -# Kavitha Branch3 change3 -# Kavitha Branch3 change4 -# Kavitha Branch3 change5 -# Kavitha Branch3 change6 -# Kavitha Branch3 change7 -# Kavitha Branch4 change1 -# Kavitha Branch4 change2 -# Kavitha Branch5 change1 -# Kavitha Branch5 change2 -# Kavitha Branch5 change3 -# Kavitha Branch5 change4 - -# Kavitha Branch6 change1 - -# Kavitha Branch7 change1 - -# Divya_branch1_change1 - - -# Divya branch51 change1 - -# Divya branch52 change1 - -# Divya branch53 change1 - -# Divya branch61 - -# bibhu_branch1 +# Dependencies: +- Linux (tested on ubuntu) +- Go (https://go.dev/doc/install) +- Bazel-5.4.0+ (https://bazel.build/install) +- Rest of the dependencies should be auto-installed on bazel run. + +# Compilation: +``` +bazel build ... +``` + +# Compile and Run Test: +``` +bazel run //tests:test_name --test_strategy=exclusive --test_timeout=3600 +``` + + +# Debug code: +- Install Delve (https://github.com/go-delve/delve/tree/master/Documentation/installation) +- Compile repo in debug mode: +``` +bazel build ... --strip=never --compilation_mode=dbg +``` +- Run the test with dlv debugger: +``` +dlv --wd=$PWD/tests/ exec bazel-bin/tests/test_name_/test_name -- --testbed=$PWD/testbeds/testbed.textproto +// inside dlv; map path for debugging: +config substitute-path external bazel-pins_ondatra/external +``` diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel new file mode 100644 index 0000000..6b582fc --- /dev/null +++ b/WORKSPACE.bazel @@ -0,0 +1,135 @@ +# 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 +# +# 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. + +workspace(name = "com_github_sonic_net_sonic_mgmt_sdn_tests_pins_ondatra") + +# -- Load buildifier ----------------------------------------------------------- +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# Bazel toolchain to build go-lang. +http_archive( + name = "io_bazel_rules_go", + sha256 = "91585017debb61982f7054c9688857a2ad1fd823fc3f9cb05048b0025c47d023", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.42.0/rules_go-v0.42.0.zip", + "https://github.com/bazelbuild/rules_go/releases/download/v0.42.0/rules_go-v0.42.0.zip", + ], +) + +# Gazelle to auto generate go-lang BUILD rules. +http_archive( + name = "bazel_gazelle", + sha256 = "b7387f72efb59f876e4daae42f1d3912d0d45563eac7cb23d1de0b094ab588cf", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.34.0/bazel-gazelle-v0.34.0.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.34.0/bazel-gazelle-v0.34.0.tar.gz", + ], +) + +load("pins_deps.bzl", "pins_deps") + +pins_deps() + +# -- Load Rules Foreign CC ----------------------------------------------------- + +load("@rules_foreign_cc//foreign_cc:repositories.bzl", "rules_foreign_cc_dependencies") + +rules_foreign_cc_dependencies() + +# -- Load GoLang Rules ----------------------------------------------------- + +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") +load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") +load("//:infra_deps.bzl", "binding_deps") + +binding_deps() + +go_rules_dependencies() + +go_register_toolchains(version = "1.21.1") + +gazelle_dependencies(go_repository_default_config = "@//:WORKSPACE.bazel") + +# -- Load GRPC ------------------------------------------------------------- + +load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language") + +switched_rules_by_language( + name = "com_google_googleapis_imports", + cc = True, + go = True, + grpc = True, +) + +load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") + +grpc_deps() + +load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps") + +grpc_extra_deps() + +# -- Load Protobuf ------------------------------------------------------------- + +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") + +protobuf_deps() + +load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") + +rules_proto_dependencies() + +rules_proto_toolchains() + +### Bazel rules for many languages to compile PROTO into gRPC libraries +http_archive( + name = "rules_proto_grpc", + sha256 = "f87d885ebfd6a1bdf02b4c4ba5bf6fb333f90d54561e4d520a8413c8d1fb7beb", + strip_prefix = "rules_proto_grpc-4.5.0", + urls = ["https://github.com/rules-proto-grpc/rules_proto_grpc/archive/4.5.0.tar.gz"], + patch_args = ["-p1"], + patches = [ + "//:bazel/patches/rules_proto_grpc.patch", + ], +) + +load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_repos", "rules_proto_grpc_toolchains") + +rules_proto_grpc_toolchains() + +rules_proto_grpc_repos() + +# -- Load P4Runtime ------------------------------------------------------------ + +load("@com_github_p4lang_p4runtime//:p4runtime_deps.bzl", "p4runtime_deps") + +p4runtime_deps() + +# -- Load packaging rules ------------------------------------------------------ + +load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies") + +rules_pkg_dependencies() + +# == Dependencies needed for testing and formatting only ======================= + +# -- Load p4c ------------------------------------------------------------------ + +load("@com_github_p4lang_p4c//:bazel/p4c_deps.bzl", "p4c_deps") + +p4c_deps() + +load("@com_github_nelhage_rules_boost//:boost/boost.bzl", "boost_deps") + +boost_deps() diff --git a/bazel/patches/ghodss_yaml.patch b/bazel/patches/ghodss_yaml.patch new file mode 100644 index 0000000..011d484 --- /dev/null +++ b/bazel/patches/ghodss_yaml.patch @@ -0,0 +1,12 @@ +diff --git a/BUILD.bazel b/BUILD.bazel +index 4f4ecec..ee196e8 100644 +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -6,6 +6,7 @@ go_library( + "fields.go", + "yaml.go", + ], ++ deps = ["@in_gopkg_yaml_v2//:yaml_v2"], + importpath = "github.com/ghodss/yaml", + visibility = ["//visibility:public"], + ) diff --git a/bazel/patches/gnmi-001-fix_virtual_proto_import.patch b/bazel/patches/gnmi-001-fix_virtual_proto_import.patch new file mode 100644 index 0000000..ef6197e --- /dev/null +++ b/bazel/patches/gnmi-001-fix_virtual_proto_import.patch @@ -0,0 +1,37 @@ +diff --git a/proto/gnmi/BUILD.bazel b/proto/gnmi/BUILD.bazel +index f471488..14a242b 100755 +--- a/proto/gnmi/BUILD.bazel ++++ b/proto/gnmi/BUILD.bazel +@@ -22,6 +22,17 @@ package( + licenses = ["notice"], + ) + ++proto_library( ++ name = "gnmi_internal_proto", ++ srcs = ["gnmi.proto"], ++ deps = [ ++ "//proto/gnmi_ext:gnmi_ext_proto", ++ "@com_google_protobuf//:any_proto", ++ "@com_google_protobuf//:descriptor_proto", ++ ], ++ visibility = ["//visibility:private"], ++) ++ + proto_library( + name = "gnmi_proto", + srcs = ["gnmi.proto"], +@@ -35,12 +46,12 @@ proto_library( + + cc_proto_library( + name = "gnmi_cc_proto", +- deps = [":gnmi_proto"], ++ deps = [":gnmi_internal_proto"], + ) + + cc_grpc_library( + name = "gnmi_cc_grpc_proto", +- srcs = [":gnmi_proto"], ++ srcs = [":gnmi_internal_proto"], + generate_mocks = True, + grpc_only = True, + deps = [":gnmi_cc_proto"], \ No newline at end of file diff --git a/bazel/patches/gnmi.patch b/bazel/patches/gnmi.patch new file mode 100644 index 0000000..3e62f2c --- /dev/null +++ b/bazel/patches/gnmi.patch @@ -0,0 +1,392 @@ +diff --git a/BUILD.bazel b/BUILD.bazel +index ca5484e..238dde9 100644 +--- a/BUILD.bazel ++++ b/BUILD.bazel +@@ -13,6 +13,7 @@ + # limitations under the License. + # + # Supporting infrastructure for implementing and testing PINS. ++load("@bazel_gazelle//:def.bzl", "gazelle") + + package( + default_visibility = ["//visibility:public"], +@@ -20,3 +21,6 @@ package( + ) + + exports_files(["LICENSE"]) ++ ++# gazelle:prefix github.com/openconfig/gnmi ++gazelle(name = "gazelle") +diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel +deleted file mode 100644 +index 2f385f7..0000000 +--- a/WORKSPACE.bazel ++++ /dev/null +@@ -1,45 +0,0 @@ +-# Copyright 2021 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. +- +-workspace(name = "gnmi") +- +-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +- +-http_archive( +- name = "io_bazel_rules_go", +- sha256 = "d6b2513456fe2229811da7eb67a444be7785f5323c6708b38d851d2b51e54d83", +- urls = [ +- "https://github.com/bazelbuild/rules_go/releases/download/v0.30.0/rules_go-v0.30.0.zip", +- "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.30.0/rules_go-v0.30.0.zip", +- ], +-) +- +-load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") +- +-go_rules_dependencies() +- +-go_register_toolchains(version = "1.17") +- +-# -- Load Dependencies --------------------------------------------------------- +-load("gnmi_deps.bzl", "gnmi_deps") +- +-gnmi_deps() +- +-load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") +- +-grpc_deps() +- +-load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps") +- +-grpc_extra_deps() +diff --git a/proto/gnmi/BUILD.bazel b/proto/gnmi/BUILD.bazel +index f471488..f6bd3bd 100644 +--- a/proto/gnmi/BUILD.bazel ++++ b/proto/gnmi/BUILD.bazel +@@ -16,6 +16,9 @@ + # + + load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") ++load("@rules_proto//proto:defs.bzl", "proto_library") + + package( + default_visibility = ["//visibility:public"], +@@ -45,3 +48,13 @@ cc_grpc_library( + grpc_only = True, + deps = [":gnmi_cc_proto"], + ) ++ ++go_proto_library( ++ name = "gnmi_go_proto", ++ compilers = ["@io_bazel_rules_go//proto:go_grpc"], ++ importpath = "github.com/openconfig/gnmi/proto/gnmi", ++ proto = ":gnmi_proto", ++ deps = [ ++ "//proto/gnmi_ext:gnmi_ext_go_proto", ++ ], ++) +diff --git a/proto/gnmi_ext/BUILD.bazel b/proto/gnmi_ext/BUILD.bazel +index 2e0e9b4..5dcf6fb 100644 +--- a/proto/gnmi_ext/BUILD.bazel ++++ b/proto/gnmi_ext/BUILD.bazel +@@ -14,6 +14,7 @@ + # + # Supporting infrastructure for implementing and testing PINS. + # ++load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +@@ -29,3 +30,10 @@ cc_proto_library( + name = "gnmi_ext_cc_proto", + deps = [":gnmi_ext_proto"], + ) ++ ++go_proto_library( ++ name = "gnmi_ext_go_proto", ++ compilers = ["@io_bazel_rules_go//proto:go_grpc"], ++ importpath = "github.com/openconfig/gnmi/proto/gnmi_ext", ++ proto = ":gnmi_ext_proto", ++) +\ No newline at end of file +diff --git a/errlist/BUILD.bazel b/errlist/BUILD.bazel +new file mode 100644 +index 0000000..2b112a8 +--- /dev/null ++++ b/errlist/BUILD.bazel +@@ -0,0 +1,16 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "errlist", ++ srcs = [ ++ "errlist.go", ++ ], ++ importpath = "github.com/openconfig/gnmi/errlist", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":errlist", ++ visibility = ["//visibility:public"], ++) + +diff --git a/value/BUILD.bazel b/value/BUILD.bazel +new file mode 100644 +index 0000000..1b5e851 +--- /dev/null ++++ b/value/BUILD.bazel +@@ -0,0 +1,19 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "value", ++ srcs = [ ++ "value.go", ++ ], ++ importpath = "github.com/openconfig/gnmi/value", ++ visibility = ["//visibility:public"], ++ deps = [ ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", ++ ] ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":value", ++ visibility = ["//visibility:public"], ++) + +diff --git a/cache/BUILD.bazel b/cache/BUILD.bazel +new file mode 100644 +index 0000000..07971dd +--- /dev/null ++++ b/cache/BUILD.bazel +@@ -0,0 +1,33 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "cache", ++ srcs = [ ++ "cache.go", ++ ], ++ deps = [ ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", ++ "@com_github_openconfig_gnmi//path", ++ "@com_github_openconfig_gnmi//ctree", ++ "@com_github_openconfig_gnmi//errlist", ++ "@com_github_openconfig_gnmi//value", ++ "@com_github_openconfig_gnmi//latency", ++ "@com_github_openconfig_gnmi//metadata", ++ "@org_golang_google_grpc//:go_default_library", ++ "@org_golang_google_grpc//codes:go_default_library", ++ "@org_golang_google_grpc//peer:go_default_library", ++ "@org_golang_google_grpc//status:go_default_library", ++ "@org_golang_x_net//context", ++ "@com_github_golang_glog//:glog", ++ "@org_golang_google_protobuf//encoding/prototext", ++ "@org_golang_google_protobuf//proto", ++ ], ++ importpath = "github.com/openconfig/gnmi/cache", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":cache", ++ visibility = ["//visibility:public"], ++) + +diff --git a/subscribe/BUILD.bazel b/subscribe/BUILD.bazel +new file mode 100644 +index 0000000..05b9be3 +--- /dev/null ++++ b/subscribe/BUILD.bazel +@@ -0,0 +1,35 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "subscribe", ++ srcs = [ ++ "subscribe.go", ++ "stats.go" ++ ], ++ importpath = "github.com/openconfig/gnmi/subscribe", ++ deps = [ ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", ++ "@com_github_openconfig_gnmi//path", ++ "@com_github_openconfig_gnmi//ctree", ++ "@com_github_openconfig_gnmi//errlist", ++ "@com_github_openconfig_gnmi//value", ++ "@com_github_openconfig_gnmi//latency", ++ "@com_github_openconfig_gnmi//cache", ++ "@com_github_openconfig_gnmi//coalesce", ++ "@com_github_openconfig_gnmi//match", ++ "@org_golang_google_grpc//:go_default_library", ++ "@org_golang_google_grpc//codes:go_default_library", ++ "@org_golang_google_grpc//peer:go_default_library", ++ "@org_golang_google_grpc//status:go_default_library", ++ "@org_golang_x_net//context", ++ "@org_golang_google_protobuf//proto", ++ "@com_github_golang_glog//:glog", ++ ], ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":subscribe", ++ visibility = ["//visibility:public"], ++) + +diff --git a/ctree/BUILD.bazel b/ctree/BUILD.bazel +new file mode 100644 +index 0000000..510cc34 +--- /dev/null ++++ b/ctree/BUILD.bazel +@@ -0,0 +1,16 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "ctree", ++ srcs = [ ++ "tree.go", ++ ], ++ importpath = "github.com/openconfig/gnmi/ctree", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":ctree", ++ visibility = ["//visibility:public"], ++) +diff --git a/latency/BUILD.bazel b/latency/BUILD.bazel +new file mode 100644 +index 0000000..d110090 +--- /dev/null ++++ b/latency/BUILD.bazel +@@ -0,0 +1,16 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "latency", ++ srcs = [ ++ "latency.go", ++ ], ++ importpath = "github.com/openconfig/gnmi/latency", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":latency", ++ visibility = ["//visibility:public"], ++) +diff --git a/metadata/BUILD.bazel b/metadata/BUILD.bazel +new file mode 100644 +index 0000000..aa715a9 +--- /dev/null ++++ b/metadata/BUILD.bazel +@@ -0,0 +1,19 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "metadata", ++ srcs = [ ++ "metadata.go", ++ ], ++ deps = [ ++ "@com_github_openconfig_gnmi//latency", ++ ], ++ importpath = "github.com/openconfig/gnmi/metadata", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":metadata", ++ visibility = ["//visibility:public"], ++) +diff --git a/path/BUILD.bazel b/path/BUILD.bazel +new file mode 100644 +index 0000000..65a7efd +--- /dev/null ++++ b/path/BUILD.bazel +@@ -0,0 +1,19 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "path", ++ srcs = [ ++ "path.go", ++ ], ++ deps = [ ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", ++ ], ++ importpath = "github.com/openconfig/gnmi/path", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":path", ++ visibility = ["//visibility:public"], ++) + +diff --git a/coalesce/BUILD.bazel b/coalesce/BUILD.bazel +new file mode 100644 +index 0000000..887440e +--- /dev/null ++++ b/coalesce/BUILD.bazel +@@ -0,0 +1,16 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "coalesce", ++ srcs = [ ++ "coalesce.go", ++ ], ++ importpath = "github.com/openconfig/gnmi/coalesce", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":coalesce", ++ visibility = ["//visibility:public"], ++) +diff --git a/match/BUILD.bazel b/match/BUILD.bazel +new file mode 100644 +index 0000000..b09b9f3 +--- /dev/null ++++ b/match/BUILD.bazel +@@ -0,0 +1,16 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "match", ++ srcs = [ ++ "match.go", ++ ], ++ importpath = "github.com/openconfig/gnmi/match", ++ visibility = ["//visibility:public"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":match", ++ visibility = ["//visibility:public"], ++) diff --git a/bazel/patches/gnoi.patch b/bazel/patches/gnoi.patch new file mode 100644 index 0000000..491e80c --- /dev/null +++ b/bazel/patches/gnoi.patch @@ -0,0 +1,45 @@ +diff --git a/healthz/BUILD.bazel b/healthz/BUILD.bazel +index 039f3b5..7c9940b 100644 +--- a/healthz/BUILD.bazel ++++ b/healthz/BUILD.bazel +@@ -34,7 +34,7 @@ proto_library( + ], + ) + +-go_proto_library( ++go_grpc_library( + name = "healthz_go_proto", + compilers = ["@io_bazel_rules_go//proto:go_grpc"], + importpath = "github.com/openconfig/gnoi/healthz", + +diff --git a/types/BUILD.bazel b/types/BUILD.bazel +index 921d7c1..995dd1e 100644 +--- a/types/BUILD.bazel ++++ b/types/BUILD.bazel +@@ -32,6 +32,13 @@ proto_library( + deps = ["@com_google_protobuf//:descriptor_proto"], + ) + ++proto_library( ++ name = "gnoi_types_proto", ++ srcs = ["types.proto"], ++ import_prefix = "github.com/openconfig/gnoi", ++ deps = ["@com_google_protobuf//:descriptor_proto"], ++) ++ + cc_proto_library( + name = "types_cc_proto", + deps = [":types_proto"], + +diff --git a/packet_link_qualification/BUILD.bazel b/packet_link_qualification/BUILD.bazel +index 249bc3a..d215296 100644 +--- a/packet_link_qualification/BUILD.bazel ++++ b/packet_link_qualification/BUILD.bazel +@@ -22,6 +22,6 @@ go_proto_library( + visibility = ["//visibility:public"], + deps = [ + "//types:types_go_proto", +- "@go_googleapis//google/rpc:status_go_proto", ++ "@org_golang_google_genproto//googleapis/rpc/status", + ], + ) diff --git a/bazel/patches/gnoigo.patch b/bazel/patches/gnoigo.patch new file mode 100644 index 0000000..7693d64 --- /dev/null +++ b/bazel/patches/gnoigo.patch @@ -0,0 +1,32 @@ +diff --git a/gnoigo.go b/gnoigo.go +index cc1a3ec..f656e0e 100644 +--- a/gnoigo.go ++++ b/gnoigo.go +@@ -27,10 +27,10 @@ import ( + fpb "github.com/openconfig/gnoi/file" + hpb "github.com/openconfig/gnoi/healthz" + lpb "github.com/openconfig/gnoi/layer2" ++ plqpb "github.com/openconfig/gnoi/linkqual" + mpb "github.com/openconfig/gnoi/mpls" + ospb "github.com/openconfig/gnoi/os" + otpb "github.com/openconfig/gnoi/otdr" +- plqpb "github.com/openconfig/gnoi/packet_link_qualification" + spb "github.com/openconfig/gnoi/system" + wrpb "github.com/openconfig/gnoi/wavelength_router" + "github.com/openconfig/gnoigo/internal" +diff --git a/internal/clients.go b/internal/clients.go +index f49e470..b634ab3 100644 +--- a/internal/clients.go ++++ b/internal/clients.go +@@ -23,10 +23,10 @@ import ( + fpb "github.com/openconfig/gnoi/file" + hpb "github.com/openconfig/gnoi/healthz" + lpb "github.com/openconfig/gnoi/layer2" ++ plqpb "github.com/openconfig/gnoi/linkqual" + mpb "github.com/openconfig/gnoi/mpls" + ospb "github.com/openconfig/gnoi/os" + otpb "github.com/openconfig/gnoi/otdr" +- plqpb "github.com/openconfig/gnoi/packet_link_qualification" + spb "github.com/openconfig/gnoi/system" + wrpb "github.com/openconfig/gnoi/wavelength_router" + ) diff --git a/bazel/patches/gnsi.patch b/bazel/patches/gnsi.patch new file mode 100644 index 0000000..5574f54 --- /dev/null +++ b/bazel/patches/gnsi.patch @@ -0,0 +1,59 @@ +diff --git a/authz/authz.proto b/authz/authz.proto +index ecb3f3f..c6216b8 100644 +--- a/authz/authz.proto ++++ b/authz/authz.proto +@@ -132,6 +132,9 @@ service Authz { + // together with its version and created-on information. + // If no policy has been set, Get() returns FAILED_PRECONDITION. + rpc Get(GetRequest) returns (GetResponse); ++ ++ rpc Install(stream InstallAuthzRequest) ++ returns (stream InstallAuthzResponse); + } + + // Request messages to rotate existing gRPC-level Authorization Policy on +@@ -152,6 +155,16 @@ message RotateAuthzRequest { + bool force_overwrite = 3; + } + ++// Request messages to install a new Authz Policy on ++// the target. ++message InstallAuthzRequest { ++ // Request Messages. ++ oneof install_request { ++ UploadRequest upload_request = 1; ++ FinalizeRequest finalize_installation = 2; ++ } ++} ++ + // Response messages from the target. + message RotateAuthzResponse { + // Response messages. +@@ -160,6 +173,14 @@ message RotateAuthzResponse { + } + } + ++// Response messages from the target. ++message InstallAuthzResponse { ++ // Response messages. ++ oneof install_response { ++ UploadResponse upload_response = 1; ++ } ++} ++ + // A Finalize message is sent to the target to confirm the rotation of + // the gRPC-level Authorization Policy, indicating that it should not be + // rolled back when the stream concludes. +diff --git a/version/BUILD.bazel b/version/BUILD.bazel +index e047013..5dbb1c6 100644 +--- a/version/BUILD.bazel ++++ b/version/BUILD.bazel +@@ -11,7 +11,7 @@ proto_library( + srcs = [ + "version.proto", + ], +- deps = ["@com_github_openconfig_gnoi//types:types_proto"], ++ deps = ["@com_github_openconfig_gnoi//types:gnoi_types_proto"], + import_prefix = "github.com/openconfig/gnsi", + visibility = ["//visibility:public"], + ) diff --git a/bazel/patches/gribi.patch b/bazel/patches/gribi.patch new file mode 100644 index 0000000..e129cfd --- /dev/null +++ b/bazel/patches/gribi.patch @@ -0,0 +1,48 @@ +diff --git a/v1/proto/gribi_aft/BUILD.bazel b/v1/proto/gribi_aft/BUILD.bazel +index fdb39a2..3ddfc19 100644 +--- a/v1/proto/gribi_aft/BUILD.bazel ++++ b/v1/proto/gribi_aft/BUILD.bazel +@@ -7,8 +7,8 @@ proto_library( + srcs = ["gribi_aft.proto"], + visibility = ["//visibility:public"], + deps = [ +- "//github.com/openconfig/ygot/proto/yext:yext_proto", +- "//github.com/openconfig/ygot/proto/ywrapper:ywrapper_proto", ++ "@com_github_openconfig_ygot//proto/yext:yext_proto", ++ "@com_github_openconfig_ygot//proto/ywrapper:ywrapper_proto", + "//v1/proto/gribi_aft/enums:enums_proto", + ], + ) +@@ -19,8 +19,8 @@ go_proto_library( + proto = ":gribi_aft_proto", + visibility = ["//visibility:public"], + deps = [ +- "//github.com/openconfig/ygot/proto/yext:yext_proto", +- "//github.com/openconfig/ygot/proto/ywrapper:ywrapper_proto", ++ "@com_github_openconfig_ygot//proto/yext:go_default_library", ++ "@com_github_openconfig_ygot//proto/ywrapper:go_default_library", + "//v1/proto/gribi_aft/enums", + ], + ) +diff --git a/v1/proto/gribi_aft/enums/BUILD.bazel b/v1/proto/gribi_aft/enums/BUILD.bazel +index 7ef4d9d..18f7324 100644 +--- a/v1/proto/gribi_aft/enums/BUILD.bazel ++++ b/v1/proto/gribi_aft/enums/BUILD.bazel +@@ -6,7 +6,7 @@ proto_library( + name = "enums_proto", + srcs = ["enums.proto"], + visibility = ["//visibility:public"], +- deps = ["//github.com/openconfig/ygot/proto/yext:yext_proto"], ++ deps = ["@com_github_openconfig_ygot//proto/yext:yext_proto"], + ) + + go_proto_library( +@@ -14,7 +14,7 @@ go_proto_library( + importpath = "github.com/openconfig/gribi/v1/proto/gribi_aft/enums", + proto = ":enums_proto", + visibility = ["//visibility:public"], +- deps = ["//github.com/openconfig/ygot/proto/yext:yext_proto"], ++ deps = ["@com_github_openconfig_ygot//proto/yext:go_default_library"], + ) + + go_library( diff --git a/bazel/patches/grpc-001-fix_file_watcher_race_condition.patch b/bazel/patches/grpc-001-fix_file_watcher_race_condition.patch new file mode 100644 index 0000000..af5d151 --- /dev/null +++ b/bazel/patches/grpc-001-fix_file_watcher_race_condition.patch @@ -0,0 +1,12 @@ +diff --git a/src/core/lib/iomgr/load_file.cc b/src/core/lib/iomgr/load_file.cc +index 9068670118..a4d9bc95b2 100644 +--- a/src/core/lib/iomgr/load_file.cc ++++ b/src/core/lib/iomgr/load_file.cc +@@ -55,7 +55,6 @@ grpc_error_handle grpc_load_file(const char* filename, int add_null_terminator, + if (bytes_read < contents_size) { + gpr_free(contents); + error = GRPC_OS_ERROR(errno, "fread"); +- GPR_ASSERT(ferror(file)); + goto end; + } + if (add_null_terminator) { diff --git a/bazel/patches/grpc-003-fix_go_gazelle_register_toolchain.patch b/bazel/patches/grpc-003-fix_go_gazelle_register_toolchain.patch new file mode 100644 index 0000000..aeefa3d --- /dev/null +++ b/bazel/patches/grpc-003-fix_go_gazelle_register_toolchain.patch @@ -0,0 +1,12 @@ +diff --git a/bazel/grpc_extra_deps.bzl b/bazel/grpc_extra_deps.bzl +index 4d8afa3..b090036 100755 +--- a/bazel/grpc_extra_deps.bzl ++++ b/bazel/grpc_extra_deps.bzl +@@ -53,7 +53,6 @@ def grpc_extra_deps(ignore_version_differences = False): + api_dependencies() + + go_rules_dependencies() +- go_register_toolchains(version = "1.18") + gazelle_dependencies() + + # Pull-in the go 3rd party dependencies for protoc_gen_validate, which is diff --git a/bazel/patches/ondatra.patch b/bazel/patches/ondatra.patch new file mode 100644 index 0000000..eee421e --- /dev/null +++ b/bazel/patches/ondatra.patch @@ -0,0 +1,52 @@ +diff --git a/binding/abstract.go b/binding/abstract.go +index 4d431d1..0ff43a4 100644 +--- a/binding/abstract.go ++++ b/binding/abstract.go +@@ -33,7 +33,7 @@ import ( + credzpb "github.com/openconfig/gnsi/credentialz" + pathzpb "github.com/openconfig/gnsi/pathz" + +- grpb "github.com/openconfig/gribi/v1/proto/service" ++ grpb "github.com/openconfig/gribi/proto/service" + opb "github.com/openconfig/ondatra/proto" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" + ) +diff --git a/binding/binding.go b/binding/binding.go +index 2a4d7ae..96229b5 100644 +--- a/binding/binding.go ++++ b/binding/binding.go +@@ -33,7 +33,7 @@ import ( + certzpb "github.com/openconfig/gnsi/certz" + credzpb "github.com/openconfig/gnsi/credentialz" + pathzpb "github.com/openconfig/gnsi/pathz" +- grpb "github.com/openconfig/gribi/v1/proto/service" ++ grpb "github.com/openconfig/gribi/proto/service" + opb "github.com/openconfig/ondatra/proto" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" + ) +diff --git a/internal/rawapis/rawapis.go b/internal/rawapis/rawapis.go +index bef545c..98df921 100644 +--- a/internal/rawapis/rawapis.go ++++ b/internal/rawapis/rawapis.go +@@ -34,7 +34,7 @@ import ( + "google.golang.org/grpc" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +- grpb "github.com/openconfig/gribi/v1/proto/service" ++ grpb "github.com/openconfig/gribi/proto/service" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" + ) + +diff --git a/raw/raw.go b/raw/raw.go +index 780e978..1821141 100644 +--- a/raw/raw.go ++++ b/raw/raw.go +@@ -89,7 +89,7 @@ import ( + "github.com/openconfig/ondatra/internal/rawapis" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +- grpb "github.com/openconfig/gribi/v1/proto/service" ++ grpb "github.com/openconfig/gribi/proto/service" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" + ) + diff --git a/bazel/patches/p4lang.patch b/bazel/patches/p4lang.patch new file mode 100644 index 0000000..f6cc319 --- /dev/null +++ b/bazel/patches/p4lang.patch @@ -0,0 +1,25 @@ +diff --git a/proto/p4/config/v1/p4info.proto b/proto/p4/config/v1/p4info.proto +index badddd9..079f258 100644 +--- a/proto/p4/config/v1/p4info.proto ++++ b/proto/p4/config/v1/p4info.proto +@@ -15,7 +15,7 @@ + syntax = "proto3"; + + import "google/protobuf/any.proto"; +-import "p4/config/v1/p4types.proto"; ++import "proto/p4/config/v1/p4types.proto"; + + // This package and its contents are a work-in-progress. + +diff --git a/go/p4/v1/BUILD.bazel b/go/p4/v1/BUILD.bazel +index 6445fff..17a350c 100644 +--- a/go/p4/v1/BUILD.bazel ++++ b/go/p4/v1/BUILD.bazel +@@ -17,6 +17,7 @@ go_library( + "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", + "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", + "@org_golang_google_protobuf//types/known/anypb:go_default_library", ++ "@com_github_p4lang_p4runtime//go/p4/config/v1:go_default_library" + ], + ) + \ No newline at end of file diff --git a/bazel/patches/rules_proto_grpc.patch b/bazel/patches/rules_proto_grpc.patch new file mode 100644 index 0000000..7d78ff8 --- /dev/null +++ b/bazel/patches/rules_proto_grpc.patch @@ -0,0 +1,39 @@ +diff --git a/c/c_proto_library.bzl b/c/c_proto_library.bzl +index ee33ebd..a35a8a5 100644 +--- a/c/c_proto_library.bzl ++++ b/c/c_proto_library.bzl +@@ -44,7 +44,7 @@ def c_proto_library(name, **kwargs): # buildifier: disable=function-docstring + linkopts = kwargs.get("linkopts"), + linkstatic = kwargs.get("linkstatic"), + local_defines = kwargs.get("local_defines"), +- nocopts = kwargs.get("nocopts"), ++ #nocopts = kwargs.get("nocopts"), + strip_include_prefix = kwargs.get("strip_include_prefix"), + **{ + k: v +diff --git a/cpp/cpp_grpc_library.bzl b/cpp/cpp_grpc_library.bzl +index 7064aa7..009a931 100644 +--- a/cpp/cpp_grpc_library.bzl ++++ b/cpp/cpp_grpc_library.bzl +@@ -44,7 +44,7 @@ def cpp_grpc_library(name, **kwargs): # buildifier: disable=function-docstring + linkopts = kwargs.get("linkopts"), + linkstatic = kwargs.get("linkstatic"), + local_defines = kwargs.get("local_defines"), +- nocopts = kwargs.get("nocopts"), ++ #nocopts = kwargs.get("nocopts"), + strip_include_prefix = kwargs.get("strip_include_prefix"), + **{ + k: v +diff --git a/cpp/cpp_proto_library.bzl b/cpp/cpp_proto_library.bzl +index 38e3999..556e8b1 100644 +--- a/cpp/cpp_proto_library.bzl ++++ b/cpp/cpp_proto_library.bzl +@@ -44,7 +44,7 @@ def cpp_proto_library(name, **kwargs): # buildifier: disable=function-docstring + linkopts = kwargs.get("linkopts"), + linkstatic = kwargs.get("linkstatic"), + local_defines = kwargs.get("local_defines"), +- nocopts = kwargs.get("nocopts"), ++ #nocopts = kwargs.get("nocopts"), + strip_include_prefix = kwargs.get("strip_include_prefix"), + **{ + k: v diff --git a/bazel/patches/snappi.patch b/bazel/patches/snappi.patch new file mode 100644 index 0000000..98d9439 --- /dev/null +++ b/bazel/patches/snappi.patch @@ -0,0 +1,54 @@ +diff --git a/gosnappi/BUILD.bazel b/gosnappi/BUILD.bazel +index d72ce05..91b14e9 100644 +--- a/gosnappi/BUILD.bazel ++++ b/gosnappi/BUILD.bazel +@@ -10,7 +10,17 @@ go_library( + ], + importpath = "github.com/open-traffic-generator/snappi/gosnappi", + visibility = ["//visibility:public"], +- deps = ["@org_golang_google_grpc//:go_default_library"], ++ deps = [ ++ "@com_github_ghodss_yaml//:yaml", ++ "@com_github_masterminds_semver_v3//:semver", ++ "@com_github_open_traffic_generator_snappi//gosnappi/otg:go_default_library", ++ "@org_golang_google_grpc//:go_default_library", ++ "@org_golang_google_grpc//credentials/insecure", ++ "@org_golang_google_grpc//status", ++ "@org_golang_google_protobuf//encoding/protojson", ++ "@org_golang_google_protobuf//proto", ++ "@org_golang_google_protobuf//types/known/emptypb", ++ ], + ) + + alias( + +diff --git a/gosnappi/otg/BUILD.bazel b/gosnappi/otg/BUILD.bazel +index c0c81d6..5c4fc59 100644 +--- a/gosnappi/otg/BUILD.bazel ++++ b/gosnappi/otg/BUILD.bazel +@@ -5,6 +5,7 @@ load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + proto_library( + name = "otg_proto", + srcs = ["otg.proto"], ++ import_prefix = "github.com/open-traffic-generator/snappi", + visibility = ["//visibility:public"], + deps = [ + "@com_google_protobuf//:descriptor_proto", +@@ -15,7 +16,7 @@ proto_library( + go_proto_library( + name = "otg_go_proto", + compilers = ["@io_bazel_rules_go//proto:go_grpc"], +- importpath = "./otg", ++ importpath = "github.com/open-traffic-generator/snappi/gosnappi/otg_go_proto", + proto = ":otg_proto", + visibility = ["//visibility:public"], + ) +@@ -23,7 +24,7 @@ go_proto_library( + go_library( + name = "otg", + embed = [":otg_go_proto"], +- importpath = "./otg", ++ importpath = "github.com/open-traffic-generator/snappi/gosnappi/otg", + visibility = ["//visibility:public"], + ) + diff --git a/bazel/patches/ygnmi.patch b/bazel/patches/ygnmi.patch new file mode 100644 index 0000000..d93f381 --- /dev/null +++ b/bazel/patches/ygnmi.patch @@ -0,0 +1,13 @@ +diff --git a/ygnmi/BUILD.bazel b/ygnmi/BUILD.bazel +index a152057..3a2ee21 100644 +--- a/ygnmi/BUILD.bazel ++++ b/ygnmi/BUILD.bazel +@@ -15,7 +15,7 @@ go_library( + "//internal/logutil", + "@com_github_golang_glog//:go_default_library", + "@com_github_openconfig_gnmi//errlist:go_default_library", +- "@com_github_openconfig_gnmi//proto/gnmi:go_default_library", ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gocloser//:go_default_library", + "@com_github_openconfig_goyang//pkg/yang:go_default_library", + "@com_github_openconfig_ygot//util:go_default_library", \ No newline at end of file diff --git a/bazel/patches/ygot.patch b/bazel/patches/ygot.patch new file mode 100644 index 0000000..3579b8f --- /dev/null +++ b/bazel/patches/ygot.patch @@ -0,0 +1,85 @@ +diff --git a/proto/yext/BUILD.bazel b/proto/yext/BUILD.bazel +index 4ebd593..cd1f209 100644 +--- a/proto/yext/BUILD.bazel ++++ b/proto/yext/BUILD.bazel +@@ -1,5 +1,13 @@ + load("@io_bazel_rules_go//go:def.bzl", "go_library") + ++proto_library( ++ name = "yext_proto", ++ srcs = ["yext.proto"], ++ visibility = ["//visibility:public"], ++ import_prefix = "github.com/openconfig/ygot", ++ deps = ["@com_google_protobuf//:descriptor_proto"], ++) ++ + go_library( + name = "yext", + srcs = [ +@@ -20,3 +28,4 @@ alias( + actual = ":yext", + visibility = ["//visibility:public"], + ) ++ +diff --git a/proto/ywrapper/BUILD.bazel b/proto/ywrapper/BUILD.bazel +index 4537c63..51fb410 100644 +--- a/proto/ywrapper/BUILD.bazel ++++ b/proto/ywrapper/BUILD.bazel +@@ -1,5 +1,12 @@ + load("@io_bazel_rules_go//go:def.bzl", "go_library") + ++proto_library( ++ name = "ywrapper_proto", ++ srcs = ["ywrapper.proto"], ++ visibility = ["//visibility:public"], ++ import_prefix = "github.com/openconfig/ygot", ++) ++ + go_library( + name = "ywrapper", + srcs = [ +@@ -19,3 +26,4 @@ alias( + actual = ":ywrapper", + visibility = ["//visibility:public"], + ) ++ + +diff --git a/util/BUILD.bazel b/util/BUILD.bazel +index af907f2..8b45361 100644 +--- a/util/BUILD.bazel ++++ b/util/BUILD.bazel +@@ -18,7 +18,7 @@ go_library( + "//internal/yreflect", + "@com_github_golang_glog//:go_default_library", + "@com_github_kylelemons_godebug//pretty:go_default_library", +- "@com_github_openconfig_gnmi//proto/gnmi:go_default_library", ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_goyang//pkg/yang:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", + ], +diff --git a/ygot/BUILD.bazel b/ygot/BUILD.bazel +index 96d93c2..7023807 100644 +--- a/ygot/BUILD.bazel ++++ b/ygot/BUILD.bazel +@@ -20,7 +20,7 @@ go_library( + "//util", + "@com_github_kylelemons_godebug//pretty:go_default_library", + "@com_github_openconfig_gnmi//errlist:go_default_library", +- "@com_github_openconfig_gnmi//proto/gnmi:go_default_library", ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnmi//value:go_default_library", + "@com_github_openconfig_goyang//pkg/yang:go_default_library", + "@org_golang_google_protobuf//encoding/prototext:go_default_library", +diff --git a/ytypes/BUILD.bazel b/ytypes/BUILD.bazel +index d468783..99d604c 100644 +--- a/ytypes/BUILD.bazel ++++ b/ytypes/BUILD.bazel +@@ -35,7 +35,7 @@ go_library( + "//ygot", + "@com_github_golang_glog//:go_default_library", + "@com_github_kylelemons_godebug//pretty:go_default_library", +- "@com_github_openconfig_gnmi//proto/gnmi:go_default_library", ++ "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_goyang//pkg/yang:go_default_library", + "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//status:go_default_library", diff --git a/infra_deps.bzl b/infra_deps.bzl new file mode 100644 index 0000000..8dd7d88 --- /dev/null +++ b/infra_deps.bzl @@ -0,0 +1,338 @@ +load("@bazel_gazelle//:deps.bzl", "go_repository") +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") + +def binding_deps(): + """Sets up 3rd party workspaces needed to build ondatra infrastructure.""" + + # repo_map maps repo to alternate repo names. Add mapping to resolve gazelle repo name conflicts. + repo_map = { + "@com_github_p4lang_p4runtime": "@com_github_p4lang_golang_p4runtime", + "@go_googleapis": "@com_google_googleapis", + } + + build_directives = [ + "gazelle:resolve go github.com/openconfig/gnmi/proto/gnmi @com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/bgp @com_github_openconfig_gnoi//bgp:bgp_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/cert @com_github_openconfig_gnoi//cert:cert_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/diag @com_github_openconfig_gnoi//diag:diag_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/factory_reset @com_github_openconfig_gnoi//factory_reset:factory_reset_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/healthz @com_github_openconfig_gnoi//healthz:healthz_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/layer2 @com_github_openconfig_gnoi//layer2:layer2_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/os @com_github_openconfig_gnoi//os:os_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/file @com_github_openconfig_gnoi//file:file_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/mpls @com_github_openconfig_gnoi//mpls:mpls_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/otdr @com_github_openconfig_gnoi//otdr:otdr_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/system @com_github_openconfig_gnoi//system:system_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/wavelength_router @com_github_openconfig_gnoi//wavelength_router:wavelength_router_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/packet_link_qualification @com_github_openconfig_gnoi//packet_link_qualification:linkqual_go_proto", + "gazelle:resolve go github.com/openconfig/gnoi/linkqual @com_github_openconfig_gnoi//packet_link_qualification:linkqual_go_proto", + "gazelle:resolve go github.com/openconfig/gnsi/acctz @com_github_openconfig_gnsi//acctz:acctz_go_proto", + "gazelle:resolve go github.com/openconfig/gnsi/pathz @com_github_openconfig_gnsi//pathz:pathz_go_proto", + "gazelle:resolve go github.com/openconfig/gnsi/credentialz @com_github_openconfig_gnsi//credentialz:credentialz", + "gazelle:resolve go github.com/openconfig/gribi/v1/proto/service @com_github_openconfig_gribi//v1/proto/service:go_default_library", + "gazelle:resolve go github.com/p4lang/p4runtime/go/p4/v1 @com_github_p4lang_p4runtime//go/p4/v1:go_default_library", + "gazelle:resolve go github.com/openconfig/gnsi/authz @com_github_openconfig_gnsi//authz", + "gazelle:resolve go github.com/openconfig/gnsi/certz @com_github_openconfig_gnsi//certz", + "gazelle:resolve go github.com/open-traffic-generator/snappi/gosnappi @com_github_open_traffic_generator_snappi//gosnappi:go_default_library", + "gazelle:resolve go github.com/openconfig/gnoi/types @com_github_openconfig_gnoi//types:types_go_proto", + "gazelle:resolve go google.golang.org/genproto/googleapis/rpc/status @org_golang_google_genproto//googleapis/rpc/status:status", + ] + + go_repository( + name = "com_github_ghodss_yaml", + importpath = "github.com/ghodss/yaml", + repo_mapping = repo_map, + sum = "h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=", + version = "v1.0.0", + patches = ["//:bazel/patches/ghodss_yaml.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_golang_glog", + importpath = "github.com/golang/glog", + repo_mapping = repo_map, + sum = "h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=", + version = "v1.0.0", + ) + + go_repository( + name = "com_github_golang_groupcache", + importpath = "github.com/golang/groupcache", + repo_mapping = repo_map, + sum = "h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=", + version = "v0.0.0-20210331224755-41bb18bfe9da", + ) + + go_repository( + name = "com_github_golang_protobuf", + importpath = "github.com/golang/protobuf", + repo_mapping = repo_map, + sum = "h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=", + version = "v1.5.3", + ) + + go_repository( + name = "com_github_google_go_cmp", + importpath = "github.com/google/go-cmp", + repo_mapping = repo_map, + sum = "h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=", + version = "v0.5.9", + ) + + go_repository( + name = "com_github_google_gopacket", + importpath = "github.com/google/gopacket", + sum = "h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=", + version = "v1.1.19", + ) + + go_repository( + name = "com_github_kylelemons_godebug", + importpath = "github.com/kylelemons/godebug", + repo_mapping = repo_map, + sum = "h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=", + version = "v1.1.0", + ) + + go_repository( + name = "com_github_masterminds_semver_v3", + importpath = "github.com/Masterminds/semver/v3", + repo_mapping = repo_map, + sum = "h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=", + version = "v3.2.1", + ) + + go_repository( + name = "com_github_openconfig_ondatra", + importpath = "github.com/openconfig/ondatra", + repo_mapping = repo_map, + build_file_proto_mode = "disable", + build_directives = build_directives, + patches = ["//:bazel/patches/ondatra.patch"], + patch_args = ["-p1"], + commit = "c22622bbf6da04c44fe4bdc77c31c0001b8a5593", #main as of 12/18/2023 + ) + + go_repository( + name = "com_github_open_traffic_generator_snappi", + importpath = "github.com/open-traffic-generator/snappi", + repo_mapping = repo_map, + commit = "c39ebe4b4cc4a0f63f2ed14b27e14ac51ec32b5d", # v0.13.3 + patches = ["//:bazel/patches/snappi.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_gnmi", + build_file_proto_mode = "disable", + importpath = "github.com/openconfig/gnmi", + repo_mapping = repo_map, + commit = "5473f2ef722ee45c3f26eee3f4a44a7d827e3575", #v0.10.0 + patches = ["//:bazel/patches/gnmi.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_ygnmi", + importpath = "github.com/openconfig/ygnmi", + build_file_proto_mode = "disable", + commit = "c4957ab3f1a1c9ff0a6baacf94a1e25a595a9f79", # v0.11.0 + patches = ["//:bazel/patches/ygnmi.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_gnoi", + build_file_proto_mode = "disable", + importpath = "github.com/openconfig/gnoi", + repo_mapping = repo_map, + commit = "97f56280571337f6122b8c30c6bdd93368c57b54", # v0.3.0 + patches = ["//:bazel/patches/gnoi.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_gnoigo", + build_file_proto_mode = "disable", + importpath = "github.com/openconfig/gnoigo", + repo_mapping = repo_map, + build_directives = build_directives, + commit = "87413fdb22e732d9935c0b2de0567e3e09d5318b", #main as of 12/18/2023 + patches = ["//:bazel/patches/gnoigo.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_gnsi", + build_file_proto_mode = "disable", + importpath = "github.com/openconfig/gnsi", + repo_mapping = repo_map, + commit = "d5abc2e8fa51d7b57b49511655b71422638ce8cf", + patches = ["//:bazel/patches/gnsi.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_gocloser", + importpath = "github.com/openconfig/gocloser", + repo_mapping = repo_map, + sum = "h1:NSYuxdlOWLldNpid1dThR6Dci96juXioUguMho6aliI=", + version = "v0.0.0-20220310182203-c6c950ed3b0b", + ) + + go_repository( + name = "com_github_openconfig_goyang", + importpath = "github.com/openconfig/goyang", + repo_mapping = repo_map, + commit = "5ad0d2feb9ce655fb39e414bd4e3696356780cdb" # v1.4.4 + ) + + go_repository( + name = "com_github_openconfig_gribi", + importpath = "github.com/openconfig/gribi", + repo_mapping = repo_map, + commit = "635d8ce0fd7673c29ddba927c32b834e313d575c", # v1.0.0 + patches = ["//:bazel/patches/gribi.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_ygot", + importpath = "github.com/openconfig/ygot", + repo_mapping = repo_map, + build_file_proto_mode = "disable", + commit = "8efc81471e0fe679c453aa0e8c03d752721733bc", # v0.29.17 + patches = ["//:bazel/patches/ygot.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_p4lang_golang_p4runtime", + importpath = "github.com/p4lang/p4runtime", + repo_mapping = repo_map, + build_file_proto_mode = "disable", + commit = "a6f035f8ddea4fb22b2244afb59e3223dc5c1f69", + patches = ["//:bazel/patches/p4lang.patch"], + patch_args = ["-p1"], + ) + + go_repository( + name = "com_github_openconfig_testt", + importpath = "github.com/openconfig/testt", + commit = "efbb1a32ec07fa7f0b6cf7cda977fa1c584154d6", + ) + + go_repository( + name = "in_gopkg_yaml_v2", + importpath = "gopkg.in/yaml.v2", + repo_mapping = repo_map, + sum = "h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=", + version = "v2.4.0", + ) + + go_repository( + name = "io_opencensus_go", + importpath = "go.opencensus.io", + repo_mapping = repo_map, + sum = "h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=", + version = "v0.24.0", + ) + + go_repository( + name = "org_golang_google_grpc", + importpath = "google.golang.org/grpc", + repo_mapping = repo_map, + sum = "h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=", + version = "v1.54.0", + ) + + go_repository( + name = "org_golang_google_grpc_cmd_protoc_gen_go_grpc", + importpath = "google.golang.org/grpc/cmd/protoc-gen-go-grpc", + repo_mapping = repo_map, + sum = "h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=", + version = "v1.1.0", + ) + + go_repository( + name = "org_golang_google_protobuf", + importpath = "google.golang.org/protobuf", + repo_mapping = repo_map, + sum = "h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=", + version = "v1.30.0", + ) + + go_repository( + name = "org_golang_x_exp", + importpath = "golang.org/x/exp", + commit = "aacd6d4b4611949ff7dcca7a0118e9312168a5f8", + ) + + go_repository( + name = "org_golang_x_net", + importpath = "golang.org/x/net", + repo_mapping = repo_map, + sum = "h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=", + version = "v0.9.0", + ) + + go_repository( + name = "org_golang_x_sync", + importpath = "golang.org/x/sync", + repo_mapping = repo_map, + tag = "v0.3.0", + ) + + go_repository( + name = "org_golang_x_sys", + importpath = "golang.org/x/sys", + repo_mapping = repo_map, + sum = "h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=", + version = "v0.7.0", + ) + + go_repository( + name = "org_golang_x_text", + importpath = "golang.org/x/text", + repo_mapping = repo_map, + sum = "h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=", + version = "v0.9.0", + ) + + + git_repository( + name = "com_google_googleapis", + remote = "https://github.com/googleapis/googleapis", + commit = "c4915db59896a1da45b55507ece2ebc1d53ef6f5", + shallow_since = "1642638275 -0800", + ) + + + go_repository( + name = "com_github_jstemmer_go_junit_report_v2", + importpath = "github.com/jstemmer/go-junit-report/v2", + sum = "h1:BVBb1o0TfOuRCMykVAYJ1r2yoZ+ByE0f19QNF4ngQ0M=", + version = "v2.0.1-0.20220823220451-7b10b4285462", + ) + + go_repository( + name = "com_github_patrickmn_go_cache", + importpath = "github.com/patrickmn/go-cache", + sum = "h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=", + version = "v2.1.0+incompatible", + ) + + go_repository( + name = "com_github_pkg_errors", + importpath = "github.com/pkg/errors", + sum = "h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=", + version = "v0.9.1", + ) + + go_repository( + name = "com_github_pkg_sftp", + importpath = "github.com/pkg/sftp", + sum = "h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=", + version = "v1.13.1", + ) diff --git a/infrastructure/binding/BUILD.bazel b/infrastructure/binding/BUILD.bazel new file mode 100644 index 0000000..9519cd1 --- /dev/null +++ b/infrastructure/binding/BUILD.bazel @@ -0,0 +1,57 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +go_library( + name = "pinsbind", + testonly = True, + srcs = ["pins_binding.go"], + importpath = "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind", + deps = [ + "//infrastructure/binding:bindingbackend", + "//infrastructure/binding:pinsbackend", + "@com_github_golang_glog//:glog", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnoigo//:gnoigo", + "@com_github_openconfig_ondatra//binding", + "@com_github_openconfig_ondatra//binding/grpcutil", + "@com_github_openconfig_ondatra//proto:go_default_library", + "@com_github_openconfig_ondatra//proxy", + "@com_github_openconfig_ondatra//proxy/proto/reservation:go_default_library", + "@com_github_p4lang_golang_p4runtime//go/p4/v1:p4", + "@org_golang_google_grpc//:go_default_library", + ], +) + +go_library( + name = "bindingbackend", + testonly = True, + srcs = ["binding_backend.go"], + importpath = "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/bindingbackend", + deps = [ + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//binding", + "@com_github_openconfig_ondatra//proto:go_default_library", + "@org_golang_google_grpc//:go_default_library", + ], +) + +go_library( + name = "pinsbackend", + testonly = True, + srcs = ["pins_backend.go"], + importpath = "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbackend", + deps = [ + "//infrastructure/binding:bindingbackend", + "@com_github_golang_glog//:glog", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//binding", + "@com_github_openconfig_ondatra//proto:go_default_library", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//credentials", + "@org_golang_google_grpc//credentials/insecure", + ], +) diff --git a/infrastructure/binding/binding_backend.go b/infrastructure/binding/binding_backend.go new file mode 100644 index 0000000..69bd7b2 --- /dev/null +++ b/infrastructure/binding/binding_backend.go @@ -0,0 +1,84 @@ +// Package bindingbackend describes the interface to interact with the reservations and devices. +package bindingbackend + +import ( + "context" + "time" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra/binding" + opb "github.com/openconfig/ondatra/proto" + "google.golang.org/grpc" +) + +// ReservedTestbed contains information about reserved testbed. +type ReservedTestbed struct { + id string // Reservation id + name string +} + +// Device contains data of reserved switch. +type Device struct { + Name string + ID string + PortMap map[string]*binding.Port +} + +// GRPCService represents supported grpc service. +type GRPCService string + +const ( + // GNMI represents gnmi grpc service. + GNMI GRPCService = "gnmi" + // GNOI represents gnoi grpc service. + GNOI GRPCService = "gnoi" + // GNSI represents gnsi grpc service. + GNSI GRPCService = "gnsi" + // P4RT represents p4rt grpc service. + P4RT GRPCService = "p4rt" +) + +// GRPCServices contains addresses for services using grpc protocol. +type GRPCServices struct { + Addr map[GRPCService]string +} + +// HTTPService contains addresses for services using HTTP protocol. +type HTTPService struct { + Addr string +} + +// DUTDevice contains device and service addresses for DUT device. +type DUTDevice struct { + *Device + GRPC GRPCServices +} + +// ATEDevice contains device and service addresses for ATE device. +type ATEDevice struct { + *Device + HTTP HTTPService +} + +// ReservedTopology represents the reserved DUT and ATE devices. +type ReservedTopology struct { + ID string + DUTs []*DUTDevice + ATEs []*ATEDevice +} + +// Backend exposes functions to interact with reservations and reserved devices. +type Backend interface { + // ReserveTopology returns topology of reserved DUT and ATE devices. + ReserveTopology(ctx context.Context, tb *opb.Testbed, runtime, waittime time.Duration) (*ReservedTopology, error) + // Release releases the reserved devices, called during teardown. + Release(ctx context.Context) error + // DialGRPC connects to grpc service and returns the opened grpc client for use. + DialGRPC(ctx context.Context, addr string, opts ...grpc.DialOption) (*grpc.ClientConn, error) + DialConsole(ctx context.Context, dut *binding.AbstractDUT) (binding.ConsoleClient, error) + // GNMIClient wraps the grpc connection under gnmi client. + GNMIClient(ctx context.Context, dut *binding.AbstractDUT, conn *grpc.ClientConn) (gpb.GNMIClient, error) + + // Close closes backend's internal objects. + Close() error +} diff --git a/infrastructure/binding/pins_backend.go b/infrastructure/binding/pins_backend.go new file mode 100644 index 0000000..54082d2 --- /dev/null +++ b/infrastructure/binding/pins_backend.go @@ -0,0 +1,215 @@ +// Package pinsbackend can reserve Ondatra DUTs and provide clients to interact with the DUTs. +package pinsbackend + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "time" + "flag" + + log "github.com/golang/glog" + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra/binding" + opb "github.com/openconfig/ondatra/proto" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/bindingbackend" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + supportedSecurityModes = []string{"insecure", "mtls"} + securityMode = flag.String("security_mode", "insecure", fmt.Sprintf("define the security mode of the conntections to gnmi server, choose from : %v. Uses insecure as default.", supportedSecurityModes)) + ) + +// Backend can reserve Ondatra DUTs and provide clients to interact with the DUTs. +type Backend struct { + configs map[string]*tls.Config +} + +// New creates a backend object. +func New() *Backend { + return &Backend{configs: map[string]*tls.Config{}} +} + +// registerGRPCTLS caches grpc TLS certificates for the given serverName. +func (b *Backend) registerGRPCTLS(grpc *bindingbackend.GRPCServices, serverName string) error { + if serverName == "" { + return fmt.Errorf("serverName is empty") + } + + // Load certificate of the CA who signed server's certificate. + pemServerCA, err := os.ReadFile("ondatra/certs/ca_crt.pem") + if err != nil { + return err + } + + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(pemServerCA) { + return fmt.Errorf("failed to add server CA's certificate") + } + // Load client's certificate and private key + clientCert, err := tls.LoadX509KeyPair("ondatra/certs/client_crt.pem", "ondatra/certs/client_key.pem") + if err != nil { + return err + } + + for _, service := range grpc.Addr { + b.configs[service] = &tls.Config{ + Certificates: []tls.Certificate{clientCert}, + RootCAs: certPool, + ServerName: serverName, + MinVersion: tls.VersionTLS13, + } + } + + return nil +} + + +// ReserveTopology returns topology containing reserved DUT and ATE devices. +func (b *Backend) ReserveTopology(ctx context.Context, tb *opb.Testbed, runtime, waitTime time.Duration) (*bindingbackend.ReservedTopology, error) { + // Fill in the Dut and Control device details. + dut := "192.168.0.1" // sample dut address. + control := "192.168.0.2" // sample control address. + log.Infof("testbed Dut:%s Control switch:%s", dut, control) + + grpcPort := "9339" + p4rtPort := "9559" + dutGRPCAddr := fmt.Sprintf("%v:%v", dut, grpcPort) + dutP4RTAddr := fmt.Sprintf("%v:%v", dut, p4rtPort) + controlGRPCAddr := fmt.Sprintf("%v:%v", control, grpcPort) + controlP4RTAddr := fmt.Sprintf("%v:%v", control, p4rtPort) + + // Modify the reservation based on your topology. + r := &bindingbackend.ReservedTopology{ + ID: "PINS Reservation", + DUTs: []*bindingbackend.DUTDevice{{ + Device: &bindingbackend.Device{ + ID: "DUT", + Name: dut, + PortMap: map[string]*binding.Port{ + "port1": {Name: "Ethernet1/1/1"}, + "port2": {Name: "Ethernet1/1/5"}, + "port3": {Name: "Ethernet1/2/1"}, + "port4": {Name: "Ethernet1/2/5"}, + "port5": {Name: "Ethernet1/3/1"}, + "port6": {Name: "Ethernet1/3/5"}, + "port7": {Name: "Ethernet1/4/1"}, + "port8": {Name: "Ethernet1/4/5"}, + "port9": {Name: "Ethernet1/5/1"}, + "port10": {Name: "Ethernet1/5/5"}, + "port11": {Name: "Ethernet1/6/1"}, + "port12": {Name: "Ethernet1/6/5"}, + "port13": {Name: "Ethernet1/7/1"}, + "port14": {Name: "Ethernet1/7/5"}, + "port15": {Name: "Ethernet1/8/1"}, + "port16": {Name: "Ethernet1/8/5"}, + "port17": {Name: "Ethernet1/9/1"}, + "port18": {Name: "Ethernet1/9/5"}, + "port19": {Name: "Ethernet1/10/1"}, + "port20": {Name: "Ethernet1/10/5"}, + }, + }, + GRPC: bindingbackend.GRPCServices{ + Addr: map[bindingbackend.GRPCService]string{ + bindingbackend.GNMI: dutGRPCAddr, + bindingbackend.GNOI: dutGRPCAddr, + bindingbackend.GNSI: dutGRPCAddr, + bindingbackend.P4RT: dutP4RTAddr, + }, + }}, + { + Device: &bindingbackend.Device{ + ID: "CONTROL", + Name: control, + PortMap: map[string]*binding.Port{ + "port1": {Name: "Ethernet1/1/1"}, + "port2": {Name: "Ethernet1/1/5"}, + "port3": {Name: "Ethernet1/2/1"}, + "port4": {Name: "Ethernet1/2/5"}, + "port5": {Name: "Ethernet1/3/1"}, + "port6": {Name: "Ethernet1/3/5"}, + "port7": {Name: "Ethernet1/4/1"}, + "port8": {Name: "Ethernet1/4/5"}, + "port9": {Name: "Ethernet1/5/1"}, + "port10": {Name: "Ethernet1/5/5"}, + "port11": {Name: "Ethernet1/6/1"}, + "port12": {Name: "Ethernet1/6/5"}, + "port13": {Name: "Ethernet1/7/1"}, + "port14": {Name: "Ethernet1/7/5"}, + "port15": {Name: "Ethernet1/8/1"}, + "port16": {Name: "Ethernet1/8/5"}, + "port17": {Name: "Ethernet1/9/1"}, + "port18": {Name: "Ethernet1/9/5"}, + "port19": {Name: "Ethernet1/10/1"}, + "port20": {Name: "Ethernet1/10/5"}, + }, + }, + GRPC: bindingbackend.GRPCServices{ + Addr: map[bindingbackend.GRPCService]string{ + bindingbackend.GNMI: controlGRPCAddr, + bindingbackend.GNOI: controlGRPCAddr, + bindingbackend.GNSI: controlGRPCAddr, + bindingbackend.P4RT: controlP4RTAddr, + }, + }}, + }} + + for _, dut := range r.DUTs { + if err := b.registerGRPCTLS(&dut.GRPC, dut.Name); err != nil { + return nil, err + } + } + + return r, nil +} + +// Release releases the reserved devices, called during teardown. +func (b *Backend) Release(ctx context.Context) error { + return nil +} + +// DialGRPC connects to grpc service and returns the opened grpc client for use. +func (b *Backend) DialGRPC(ctx context.Context, addr string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { + if *securityMode == "mtls" { + tlsConfig, ok := b.configs[addr] + if !ok { + return nil, fmt.Errorf("failed to find TLS config for %s", addr) + } + + opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + } else { + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + conn, err := grpc.DialContext(ctx, addr, opts...) + if err != nil { + return nil, fmt.Errorf("DialContext(%s, %v) : %v", addr, opts, err) + } + return conn, nil +} + +// DialConsole returns a StreamClient for the DUT. +func (b *Backend) DialConsole(ctx context.Context, dut *binding.AbstractDUT) (binding.ConsoleClient, error) { + return nil, fmt.Errorf("unimplemented function") +} + +// GNMIClient wraps the grpc connection under gnmi client. +func (b *Backend) GNMIClient(ctx context.Context, dut *binding.AbstractDUT, conn *grpc.ClientConn) (gpb.GNMIClient, error) { + if conn == nil { + return nil, fmt.Errorf("conn is nil") + } + if dut == nil { + return nil, fmt.Errorf("dut is nil") + } + return gpb.NewGNMIClient(conn), nil +} + +// Close closes backend's internal objects. +func (b *Backend) Close() error { + b.configs = nil + return nil +} diff --git a/infrastructure/binding/pins_binding.go b/infrastructure/binding/pins_binding.go new file mode 100644 index 0000000..ed8250e --- /dev/null +++ b/infrastructure/binding/pins_binding.go @@ -0,0 +1,449 @@ +// Package pinsbind contains all the code related to the PINS project's binding to Ondatra. +package pinsbind + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + log "github.com/golang/glog" + + "github.com/openconfig/gnoigo" + "github.com/openconfig/ondatra/binding" + "github.com/openconfig/ondatra/binding/grpcutil" + pinsbackend "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbackend" + "google.golang.org/grpc" + + opb "github.com/openconfig/ondatra/proto" + "github.com/openconfig/ondatra/proxy" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/bindingbackend" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + + rpb "github.com/openconfig/ondatra/proxy/proto/reservation" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" +) + +var ( + // validate that the Binding fulfills both binding.Binding and proxy.Dialer + // interfaces. + _ binding.Binding = &Binding{} + _ proxy.Dialer = &Binding{} +) + +var backend bindingbackend.Backend + +// Binding is a binding for PINS switches. +type Binding struct { + resv *binding.Reservation + httpDialer func(target string) (proxy.HTTPDoCloser, error) +} + +// Option are configurable inputs to the binding. +type Option func(b *Binding) + +// WithHTTPDialer provides a custom http dialer that is capable of dialing specific targets. +func WithHTTPDialer(f func(target string) (proxy.HTTPDoCloser, error)) Option { + return func(b *Binding) { + b.httpDialer = f + } +} + +// New returns a new instance of a PINS Binding. +func New() (binding.Binding, error) { + return NewWithOpts() +} + +type httpClient struct { + *http.Client +} + +func (h *httpClient) Close() error { + return nil +} + +func defaultHTTPDialer(target string) (proxy.HTTPDoCloser, error) { + return &httpClient{http.DefaultClient}, nil +} + +// NewWithOpts returns a new instance of a PINS Binding. +func NewWithOpts(opts ...Option) (*Binding, error) { + b := &Binding{ + httpDialer: defaultHTTPDialer, + } + + for _, opt := range opts { + opt(b) + } + + if backend == nil { + backend = pinsbackend.New() + } + + return b, nil +} + +// SetBackend sets the backend for binding. +func SetBackend(b bindingbackend.Backend) { + backend = b +} + +// CloseBackend closes the backend. +func CloseBackend() { + if backend != nil { + backend.Close() + } + backend = nil +} + +// Reserve returns a testbed meeting requirements of testbed proto. +func (b *Binding) Reserve(ctx context.Context, tb *opb.Testbed, runtime, waitTime time.Duration, partial map[string]string) (*binding.Reservation, error) { + if backend == nil { + return nil, fmt.Errorf("backend is not set") + } + + if len(partial) > 0 { + return nil, fmt.Errorf("PINSBind Reserve does not yet support partial mappings") + } + + reservedtopology, err := backend.ReserveTopology(ctx, tb, runtime, waitTime) + if err != nil { + return nil, fmt.Errorf("failed to reserve topology: %v", err) + } + + resv := &binding.Reservation{ID: reservedtopology.ID, DUTs: map[string]binding.DUT{}} + for _, dut := range reservedtopology.DUTs { + resv.DUTs[dut.ID] = &pinsDUT{ + AbstractDUT: &binding.AbstractDUT{&binding.Dims{ + Name: dut.Name, + Ports: dut.PortMap, + }}, + bind: b, + grpc: dut.GRPC, + } + } + + if len(reservedtopology.ATEs) != 0 { + resv.ATEs = map[string]binding.ATE{} + } + for _, ate := range reservedtopology.ATEs { + resv.ATEs[ate.ID] = &pinsATE{ + AbstractATE: &binding.AbstractATE{&binding.Dims{ + Name: ate.Name, + Ports: ate.PortMap, + }}, + http: ate.HTTP, + } + } + + b.resv = resv + return resv, nil +} + +// Release returns the testbed to a pool of resources. +func (b *Binding) Release(ctx context.Context) error { + return backend.Release(ctx) +} + +type pinsDUT struct { + *binding.AbstractDUT + bind *Binding + grpc bindingbackend.GRPCServices +} + +type pinsATE struct { + *binding.AbstractATE + bind *Binding + http bindingbackend.HTTPService +} + +// DialGRPC will return a gRPC client conn for the target. This method should +// be used by any new service definitions which create underlying gRPC +// connections. +func (b *Binding) DialGRPC(ctx context.Context, addr string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { + if backend == nil { + return nil, fmt.Errorf("backend is not set") + } + + return backend.DialGRPC(ctx, addr, opts...) +} + +// HTTPClient returns a http client that is capable of dialing the provided target. +func (b *Binding) HTTPClient(target string) (proxy.HTTPDoCloser, error) { + return b.httpDialer(target) +} + +// DialGNMI connects directly to the switch's proxy. +func (d *pinsDUT) DialGNMI(ctx context.Context, opts ...grpc.DialOption) (gpb.GNMIClient, error) { + addr := d.grpc.Addr[bindingbackend.GNMI] + if addr == "" { + return nil, fmt.Errorf("service gnmi not registered on DUT %q", d.Name()) + } + + const defaultTimeout = time.Minute + ctx, cancel := grpcutil.WithDefaultTimeout(ctx, defaultTimeout) + defer cancel() + opts = append(opts, + grpcutil.WithUnaryDefaultTimeout(defaultTimeout), + grpcutil.WithStreamDefaultTimeout(defaultTimeout), + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*20))) + + conn, err := d.bind.DialGRPC(ctx, addr, opts...) + if err != nil { + return nil, err + } + + cli, err := backend.GNMIClient(ctx, d.AbstractDUT, conn) + if err != nil { + return nil, err + } + return &clientWrap{GNMIClient: cli}, nil +} + +type clientWrap struct { + gpb.GNMIClient +} + +// wrapValueInUpdate wraps the typed value in the provided update into a +// serialized JSON node., e.g. +// - 123 -> {"foo": 123} +// - [{"str": "one"}] -> {"foo": [{"str": "one"}]} +// - {"str": "test-string"} -> {"foo": {"str": "test-string"}} +func wrapValueInUpdate(up *gpb.Update) error { + elems := up.GetPath().GetElem() + if len(elems) == 0 { + // root path case + return nil + } + name := elems[len(elems)-1].GetName() + var i any + if err := json.Unmarshal(up.GetVal().GetJsonIetfVal(), &i); err != nil { + return fmt.Errorf("unable to unmarshal config: %v", err) + } + + // For list paths such as /interfaces/interface[name=], JSON IETF value + // needs to be an array instead of an object. Ondatra returns value for such paths + // as an object, which need to be translated into a JSON array. E.g. + // - {"str": "test-string"} -> [{"str": "test-string"}] + if len(elems[len(elems)-1].GetKey()) > 0 { + // The path is a list node. Perform translation to JSON array. + var arr []any + arr = append(arr, i) + arrVal, err := json.Marshal(arr) + if err != nil { + return fmt.Errorf("unable to marshal value %v as a JSON array: %v", arr, err) + } + if err := json.Unmarshal(arrVal, &i); err != nil { + return fmt.Errorf("unable to unmarshal JSON array config: %v", err) + } + } + js, err := json.MarshalIndent(map[string]any{name: i}, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal config with wrapping container: %v", err) + } + up.GetVal().Value = &gpb.TypedValue_JsonIetfVal{js} + return nil +} + +func (c *clientWrap) Set(ctx context.Context, in *gpb.SetRequest, opts ...grpc.CallOption) (*gpb.SetResponse, error) { + for _, up := range in.GetReplace() { + if err := wrapValueInUpdate(up); err != nil { + return nil, err + } + } + for _, up := range in.GetUpdate() { + if err := wrapValueInUpdate(up); err != nil { + return nil, err + } + } + + return c.GNMIClient.Set(ctx, in, opts...) +} + +func (c *clientWrap) Get(ctx context.Context, in *gpb.GetRequest, opts ...grpc.CallOption) (*gpb.GetResponse, error) { + return c.GNMIClient.Get(ctx, in, opts...) +} + +type subscribeClientWrap struct { + gpb.GNMI_SubscribeClient + client *clientWrap +} + +// CloseSend signals that the client has done sending messages to the server. +// Calling the CloseSend will cause PINs to close the Subscribe stream and return an +// error. Hence we overwrite this method to be no-op here. +func (sc *subscribeClientWrap) CloseSend() error { + return nil +} + +func (c *clientWrap) Subscribe(ctx context.Context, opts ...grpc.CallOption) (gpb.GNMI_SubscribeClient, error) { + sub, err := c.GNMIClient.Subscribe(ctx, opts...) + if err != nil { + return nil, err + } + return &subscribeClientWrap{GNMI_SubscribeClient: sub, client: c}, nil +} + +func (c *clientWrap) Capabilities(ctx context.Context, in *gpb.CapabilityRequest, opts ...grpc.CallOption) (*gpb.CapabilityResponse, error) { + return c.GNMIClient.Capabilities(ctx, in, opts...) +} + +// DialGNOI connects directly to the switch's proxy. +func (d *pinsDUT) DialGNOI(ctx context.Context, opts ...grpc.DialOption) (gnoigo.Clients, error) { + addr := d.grpc.Addr[bindingbackend.GNOI] + if addr == "" { + return nil, fmt.Errorf("service gnoi not registered on DUT %q", d.Name()) + } + + ctx, cancel := grpcutil.WithDefaultTimeout(ctx, 2*time.Minute) + defer cancel() + opts = append(opts, + grpcutil.WithUnaryDefaultTimeout(30*time.Second), + grpcutil.WithStreamDefaultTimeout(2*time.Minute), + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*20))) + + conn, err := d.bind.DialGRPC(ctx, addr, opts...) + if err != nil { + return nil, err + } + + log.Infof("GNOI dial success Address:%s, Switch:%s", conn.Target(), d.Name()) + return &GNOIClients{ + Clients: gnoigo.NewClients(conn), + }, nil +} + +// GNOIClients consist of the GNOI clients supported by PINs. +type GNOIClients struct { + gnoigo.Clients +} + +// DialP4RT connects directly to the switch's proxy. +func (d *pinsDUT) DialP4RT(ctx context.Context, opts ...grpc.DialOption) (p4pb.P4RuntimeClient, error) { + addr := d.grpc.Addr[bindingbackend.P4RT] + if addr == "" { + return nil, fmt.Errorf("service gnsi not registered on DUT %q", d.Name()) + } + + ctx, cancel := grpcutil.WithDefaultTimeout(ctx, 2*time.Minute) + defer cancel() + opts = append(opts, + grpcutil.WithUnaryDefaultTimeout(30*time.Second), + grpcutil.WithStreamDefaultTimeout(2*time.Minute), + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024*1024*20))) + + conn, err := d.bind.DialGRPC(ctx, addr, opts...) + if err != nil { + return nil, err + } + + log.Infof("P4RT dial success Address:%s, Switch:%s", conn.Target(), d.Name()) + return p4pb.NewP4RuntimeClient(conn), nil +} + +// DialConsole returns a StreamClient for the DUT. +func (d *pinsDUT) DialConsole(ctx context.Context) (binding.ConsoleClient, error) { + return backend.DialConsole(ctx, d.AbstractDUT) +} + +// FetchReservation unimplemented for experimental purposes. +func (*Binding) FetchReservation(context.Context, string) (*binding.Reservation, error) { + return nil, nil +} + +// Resolve will return a concrete reservation with services defined. +func (b *Binding) Resolve() (*rpb.Reservation, error) { + devices := map[string]*rpb.ResolvedDevice{} + for k, d := range b.resv.DUTs { + rD, err := b.resolveDUT(k, d.(*pinsDUT)) + if err != nil { + return nil, err + } + devices[k] = rD + } + ates := map[string]*rpb.ResolvedDevice{} + for k, a := range b.resv.ATEs { + rD, err := b.resolveATE(k, a.(*pinsATE)) + if err != nil { + return nil, err + } + ates[k] = rD + } + return &rpb.Reservation{ + Id: b.resv.ID, + Ates: ates, + Devices: devices, + }, nil +} + +func resolvePort(k string, p *binding.Port) *rpb.ResolvedPort { + return &rpb.ResolvedPort{ + Id: k, + Speed: p.Speed, + Name: p.Name, + } +} + +func (b *Binding) resolveDUT(key string, d *pinsDUT) (*rpb.ResolvedDevice, error) { + ports := map[string]*rpb.ResolvedPort{} + for k, p := range d.Ports() { + ports[k] = resolvePort(k, p) + } + services := map[string]*rpb.Service{ + "gnmi.gNMI": { + Id: "gnmi.gNMI", + Endpoint: &rpb.Service_ProxiedGrpc{ + ProxiedGrpc: &rpb.ProxiedGRPCEndpoint{ + Address: d.grpc.Addr[bindingbackend.GNMI], + Proxy: nil, + }, + }, + }, + "p4.v1.P4Runtime": { + Id: "p4.v1.P4Runtime", + Endpoint: &rpb.Service_ProxiedGrpc{ + ProxiedGrpc: &rpb.ProxiedGRPCEndpoint{ + Address: d.grpc.Addr[bindingbackend.P4RT], + Proxy: nil, + }, + }, + }, + } + return &rpb.ResolvedDevice{ + Id: key, + HardwareModel: d.HardwareModel(), + Vendor: d.Vendor(), + SoftwareVersion: d.SoftwareVersion(), + Name: d.Name(), + Ports: ports, + Services: services, + }, nil +} + +func (b *Binding) resolveATE(key string, d *pinsATE) (*rpb.ResolvedDevice, error) { + ports := map[string]*rpb.ResolvedPort{} + for k, p := range d.Ports() { + ports[k] = resolvePort(k, p) + } + services := map[string]*rpb.Service{ + "http": { + Id: "http", + Endpoint: &rpb.Service_HttpOverGrpc{ + HttpOverGrpc: &rpb.HTTPOverGRPCEndpoint{ + Address: d.http.Addr, + }, + }, + }, + } + return &rpb.ResolvedDevice{ + Id: key, + HardwareModel: d.HardwareModel(), + Vendor: d.Vendor(), + SoftwareVersion: d.SoftwareVersion(), + Name: d.Name(), + Ports: ports, + Services: services, + }, nil +} diff --git a/infrastructure/certs/BUILD.bazel b/infrastructure/certs/BUILD.bazel new file mode 100644 index 0000000..97f471e --- /dev/null +++ b/infrastructure/certs/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +filegroup( + name = "certs", + srcs = glob(["*.pem"]), +) diff --git a/infrastructure/certs/crt_gen.sh b/infrastructure/certs/crt_gen.sh new file mode 100755 index 0000000..73133a5 --- /dev/null +++ b/infrastructure/certs/crt_gen.sh @@ -0,0 +1,18 @@ +rm ca_key.pem ca_crt.pem server_key.pem server_req.pem server_crt.pem server_ext.cnf client_key.pem client_req.pem client_crt.pem client_ext.cnf + +echo subjectAltName = IP:"$1" > server_ext.cnf +# 1. Generate CA's private key and self-signed certificate +openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca_key.pem -out ca_crt.pem -subj "/C=US" + +# 2. Generate web server's private key and certificate signing request (CSR) +openssl req -newkey rsa:4096 -nodes -keyout server_key.pem -out server_req.pem -subj "/CN='$1'" + +# 3. Use CA's private key to sign web server's CSR and get back the signed certificate +openssl x509 -req -in server_req.pem -days 100 -CA ca_crt.pem -CAkey ca_key.pem -CAcreateserial -out server_crt.pem -extfile server_ext.cnf + +echo subjectAltName = IP:"$1" > client_ext.cnf +# 4. Generate client's private key and certificate signing request (CSR) +openssl req -newkey rsa:4096 -nodes -keyout client_key.pem -out client_req.pem -subj "/CN=*" + +# 5. Use CA's private key to sign client's CSR and get back the signed certificate +openssl x509 -req -in client_req.pem -days 100 -CA ca_crt.pem -CAkey ca_key.pem -CAcreateserial -out client_crt.pem -extfile client_ext.cnf diff --git a/infrastructure/data/BUILD.bazel b/infrastructure/data/BUILD.bazel new file mode 100644 index 0000000..40105d7 --- /dev/null +++ b/infrastructure/data/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +filegroup( + name = "data", + srcs = glob(["**"]), +) diff --git a/infrastructure/data/config.json b/infrastructure/data/config.json new file mode 100644 index 0000000..491247e --- /dev/null +++ b/infrastructure/data/config.json @@ -0,0 +1,3 @@ +{ + "openconfig-interfaces:interfaces" : {} +} diff --git a/infrastructure/data/p4rtconfig.prototext b/infrastructure/data/p4rtconfig.prototext new file mode 100644 index 0000000..0b9f05f --- /dev/null +++ b/infrastructure/data/p4rtconfig.prototext @@ -0,0 +1,3 @@ +pkg_info{ + name : "sampleP4" version : "1.0" arch : "arch" organization : "org" +} tables {} diff --git a/infrastructure/data/testbeds.textproto b/infrastructure/data/testbeds.textproto new file mode 100644 index 0000000..9765c11 --- /dev/null +++ b/infrastructure/data/testbeds.textproto @@ -0,0 +1,216 @@ +# proto-message: ondatra.Testbed + +# DUT device with 20 ports. +duts { + id: "DUT" + ports { + id: "port1" + } + ports { + id: "port2" + } + ports { + id: "port3" + } + ports { + id: "port4" + } + ports { + id: "port5" + } + ports { + id: "port6" + } + ports { + id: "port7" + } + ports { + id: "port8" + } + ports { + id: "port9" + } + ports { + id: "port10" + } + ports { + id: "port11" + } + ports { + id: "port12" + } + ports { + id: "port13" + } + ports { + id: "port14" + } + ports { + id: "port15" + } + ports { + id: "port16" + } + ports { + id: "port17" + } + ports { + id: "port18" + } + ports { + id: "port19" + } + ports { + id: "port20" + } +} + +# CONTROL device with 20 ports. +duts { + id: "CONTROL" + ports { + id: "port1" + } + ports { + id: "port2" + } + ports { + id: "port3" + } + ports { + id: "port4" + } + ports { + id: "port5" + } + ports { + id: "port6" + } + ports { + id: "port7" + } + ports { + id: "port8" + } + ports { + id: "port9" + } + ports { + id: "port10" + } + ports { + id: "port11" + } + ports { + id: "port12" + } + ports { + id: "port13" + } + ports { + id: "port14" + } + ports { + id: "port15" + } + ports { + id: "port16" + } + ports { + id: "port17" + } + ports { + id: "port18" + } + ports { + id: "port19" + } + ports { + id: "port20" + } +} + +# Specify the links between DUT and CONTROL. + +# Below link represents DUT:port1 mapped to CONTROL:port1 +links { + a: "DUT:port1" + b: "CONTROL:port1" +} +# Below link represents DUT:port2 mapped to CONTROL:port2 +links { + a: "DUT:port2" + b: "CONTROL:port2" +} +links { + a: "DUT:port3" + b: "CONTROL:port3" +} +links { + a: "DUT:port4" + b: "CONTROL:port4" +} +links { + a: "DUT:port5" + b: "CONTROL:port5" +} +links { + a: "DUT:port6" + b: "CONTROL:port6" +} +links { + a: "DUT:port7" + b: "CONTROL:port7" +} +links { + a: "DUT:port8" + b: "CONTROL:port8" +} +links { + a: "DUT:port9" + b: "CONTROL:port9" +} +links { + a: "DUT:port10" + b: "CONTROL:port10" +} +links { + a: "DUT:port11" + b: "CONTROL:port11" +} +links { + a: "DUT:port12" + b: "CONTROL:port12" +} +links { + a: "DUT:port13" + b: "CONTROL:port13" +} +links { + a: "DUT:port14" + b: "CONTROL:port14" +} +links { + a: "DUT:port15" + b: "CONTROL:port15" +} +links { + a: "DUT:port16" + b: "CONTROL:port16" +} +links { + a: "DUT:port17" + b: "CONTROL:port17" +} +links { + a: "DUT:port18" + b: "CONTROL:port18" +} +links { + a: "DUT:port19" + b: "CONTROL:port19" +} +links { + a: "DUT:port20" + b: "CONTROL:port20" +} diff --git a/infrastructure/testhelper/BUILD.bazel b/infrastructure/testhelper/BUILD.bazel new file mode 100644 index 0000000..7a52aa9 --- /dev/null +++ b/infrastructure/testhelper/BUILD.bazel @@ -0,0 +1,57 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +go_library( + name = "testhelper", + testonly = 1, + srcs = [ + "augment.go", + "gnmi.go", + "gnoi.go", + "lacp.go", + "p4rt.go", + "testhelper.go", + "platform_components.go", + "platform_info.go", + "port_management.go", + "results.go", + "ssh.go", + "//infrastructure/testhelper/platform_info:platform_info", + ], + data = [ + "//infrastructure/data", + ], + importpath = "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper", + deps = [ + "@com_github_golang_glog//:glog", + "@com_github_openconfig_goyang//pkg/yang:go_default_library", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnoi//healthz:healthz_go_proto", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_gnoi//types:types_go_proto", + "@com_github_openconfig_gocloser//:gocloser", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_ondatra//gnmi/oc/interfaces", + "@com_github_openconfig_ondatra//gnmi/oc/platform", + "@com_github_openconfig_ondatra//gnmi/oc/system", + "@com_github_openconfig_ygnmi//ygnmi", + "@com_github_openconfig_ygot//ygot", + "@com_github_openconfig_ygot//ytypes", + "@com_github_p4lang_golang_p4runtime//go/p4/config/v1:go_default_library", + "@com_github_p4lang_golang_p4runtime//go/p4/v1:p4", + "@com_github_pkg_errors//:errors", + "@com_github_pkg_sftp//:go_default_library", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//status", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//proto", + "@org_golang_x_crypto//ssh", + ], +) diff --git a/infrastructure/testhelper/augment.go b/infrastructure/testhelper/augment.go new file mode 100644 index 0000000..643bf9c --- /dev/null +++ b/infrastructure/testhelper/augment.go @@ -0,0 +1,1016 @@ +package testhelper + +import ( + "context" + "fmt" + "reflect" + "testing" + "time" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/goyang/pkg/yang" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ondatra/gnmi/oc/system" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/openconfig/ygot/ygot" + "github.com/openconfig/ygot/ytypes" + "google.golang.org/grpc" +) + +var globalEnumTypeMap = map[string][]reflect.Type{ + "/openconfig-platform/components/component/state/fully-qualified-name": []reflect.Type{reflect.TypeOf((*string)(nil))}, +} + +var globalEnumMap = map[string]map[int64]ygot.EnumDefinition{ + "E_Interface_HealthIndicator": { + 0: {Name: "UNSET"}, + 1: {Name: "GOOD"}, + 2: {Name: "BAD"}, + }, + "E_ResetCause_Cause": { + 0: {Name: "UNSET"}, + 1: {Name: "UNKNOWN"}, + 2: {Name: "POWER"}, + 3: {Name: "SWITCH"}, + 4: {Name: "WATCHDOG"}, + 5: {Name: "SOFTWARE"}, + 6: {Name: "EMULATOR"}, + 7: {Name: "CPU"}, + }, +} + +var globalSchemaTree = map[string]*yang.Entry{ + "System_ConfigMetaData": &yang.Entry{}, +} + +// embed validateGoStruct to support augmented go structs. +type validateGoStruct struct{} + +func (*validateGoStruct) IsYANGGoStruct() {} +func (*validateGoStruct) Validate(opts ...ygot.ValidationOption) error { return nil } +func (*validateGoStruct) ΛBelongingModule() string { return "openconfig-nested" } +func (t *validateGoStruct) ΛEnumTypeMap() map[string][]reflect.Type { return globalEnumTypeMap } + +func validateSubscribeUpdateResponse(resp *gpb.SubscribeResponse) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + response := resp.Response + if response == nil || reflect.TypeOf(response) != reflect.TypeOf((*gpb.SubscribeResponse_Update)(nil)) { + return fmt.Errorf("resp.response is nil") + } + updates := response.(*gpb.SubscribeResponse_Update).Update + if updates == nil || len(updates.Update) == 0 { + return fmt.Errorf("can't fetch updates from response") + } + val := updates.Update[0].Val + if val == nil { + return fmt.Errorf("can't fetch val from update") + } + value := val.Value + if value == nil { + return fmt.Errorf("can't fetch value from val") + } + return nil +} +func validateGetResponse(resp *gpb.GetResponse) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + if len(resp.Notification) < 1 { + return fmt.Errorf("can't fetch notifications from the response") + } + if len(resp.Notification[0].Update) < 1 { + return fmt.Errorf("can't fetch updates from the response") + } + return nil +} + +func getResponseNotificationStringExtractor(resp *gpb.GetResponse) string { + return resp.Notification[0].Update[0].Val.GetStringVal() +} +func getResponseNotificationUint64Extractor(resp *gpb.GetResponse) uint64 { + return resp.Notification[0].Update[0].Val.GetUintVal() +} +func getResponseNotificationUint32Extractor(resp *gpb.GetResponse) uint32 { + return uint32(resp.Notification[0].Update[0].Val.GetUintVal()) +} +func getResponseNotificationInt64Extractor(resp *gpb.GetResponse) int64 { + return resp.Notification[0].Update[0].Val.GetIntVal() +} +func getResponseNotificationInt32Extractor(resp *gpb.GetResponse) int32 { + return int32(resp.Notification[0].Update[0].Val.GetIntVal()) +} +func getResponseNotificationIntExtractor(resp *gpb.GetResponse) int { + return int(resp.Notification[0].Update[0].Val.GetIntVal()) +} +func getResponseNotificationDoubleExtractor(resp *gpb.GetResponse) float64 { + return resp.Notification[0].Update[0].Val.GetDoubleVal() +} + +func StringToYgnmiPath(path string) (*gpb.Path, error) { + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + return nil, fmt.Errorf("converting string to path failed : %v", err) + } + return &gpb.Path{Elem: sPath.Elem, Origin: "openconfig"}, nil +} + +func createGetReqFromPath(dutName, reqPath string) (*gpb.GetRequest, error) { + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + return nil, fmt.Errorf("converting string to path failed : %v", err) + } + req := &gpb.GetRequest{ + Prefix: &gpb.Path{ + Target: dutName, + }, + Path: []*gpb.Path{&gpb.Path{Elem: sPath.Elem, Origin: "openconfig"}}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + return req, nil +} + +func createSetReqFromPath(dutName, reqPath string, reqType string, value []byte) (*gpb.SetRequest, error) { + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + return nil, fmt.Errorf("converting string to path failed : %v", err) + } + req := &gpb.SetRequest{ + Prefix: &gpb.Path{ + Target: dutName, + }, + } + switch reqType { + case "update": + req.Update = []*gpb.Update{{ + Path: &gpb.Path{Elem: sPath.Elem, Origin: "openconfig"}, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: value}}, + }, + } + case "replace": + req.Replace = []*gpb.Update{ + { + Path: &gpb.Path{Elem: sPath.Elem, Origin: "openconfig"}, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: value}}, + }, + } + case "delete": + req.Delete = []*gpb.Path{&gpb.Path{Elem: sPath.Elem, Origin: "openconfig"}} + } + return req, nil +} + +// Doesn't exit the test on failure. +func getWithError[T any](t testing.TB, dut *ondatra.DUTDevice, reqPath string, extractor func(*gpb.GetResponse) T) (T, error) { + var ret T + if dut == nil { + return ret, fmt.Errorf("dut is nil") + } + getReq, err := createGetReqFromPath(dut.Name(), reqPath) + if err != nil { + return ret, err + } + + ctx := context.Background() + // Fetch get client using the raw gNMI client. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + return ret, fmt.Errorf("fetching gnmi client failed with err : %v", err) + } + + getResp, err := gnmiClient.Get(ctx, getReq) + if err != nil { + return ret, err + } + if err := validateGetResponse(getResp); err != nil { + return ret, err + } + return extractor(getResp), nil +} + +// Exits the test on failure. +func get[T any](t testing.TB, dut *ondatra.DUTDevice, reqPath string, extractor func(*gpb.GetResponse) T) T { + var ret T + if dut == nil { + t.Fatalf("err : dut is nil\n") + } + getReq, err := createGetReqFromPath(dut.Name(), reqPath) + if err != nil { + t.Fatalf("%v", err) + return ret + } + ctx := context.Background() + // Fetch get client using the raw gNMI client. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("fetching gnmi client failed with err : %v\n", err) + } + + getResp, err := gnmiClient.Get(ctx, getReq) + if err != nil { + t.Fatalf("error in gnmi Get, err : %v\n", err) + } + if err := validateGetResponse(getResp); err != nil { + t.Fatalf("invalid response : %v\n", err) + } + return extractor(getResp) +} + +func set[T any](t testing.TB, dut *ondatra.DUTDevice, reqPath string, value T, setType string) error { + if dut == nil { + return fmt.Errorf("err : dut is nil") + } + + var v []byte + switch o := any(value).(type) { + case string: + v = []byte("\"" + fmt.Sprintf("%v", o) + "\"") + default: + v = []byte(fmt.Sprintf("%v", o)) + } + + setReq, err := createSetReqFromPath(dut.Name(), reqPath, setType, v) + if err != nil { + return fmt.Errorf("error in set request creation, err : %v", err) + } + + ctx := context.Background() + // Fetch get client using the raw gNMI client. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + return fmt.Errorf("fetching gnmi client failed with err : %v", err) + } + + _, err = gnmiClient.Set(ctx, setReq) + if err != nil { + return fmt.Errorf("gnmi Set failed with err : %v", err) + } + + return nil +} + +// Exit of Failure +func update[T any](t testing.TB, dut *ondatra.DUTDevice, reqPath string, value T) { + if err := set(t, dut, reqPath, value, "update"); err != nil { + t.Fatalf("update failed, err : %v\n", err) + } +} + +// Exit of Failure +func replace[T any](t testing.TB, dut *ondatra.DUTDevice, reqPath string, value T) { + if err := set(t, dut, reqPath, value, "replace"); err != nil { + t.Fatalf("replace failed, err : %v\n", err) + } +} + +// Exits the test on failure. +func del(t testing.TB, dut *ondatra.DUTDevice, reqPath string) { + if dut == nil { + t.Fatalf("err : dut is nil\n") + } + + setReq, err := createSetReqFromPath(dut.Name(), reqPath, "replace", nil) + if err != nil { + t.Fatalf("error in creating delete request err : %v\n", err) + } + + ctx := context.Background() + // Fetch get client using the raw gNMI client. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("fetching gnmi client failed with err : %v\n", err) + } + + _, err = gnmiClient.Set(ctx, setReq) + if err != nil { + t.Fatalf("gnmi Set failed with err : %v\n", err) + } +} + +// await observes values at Query with a STREAM subscription, +// blocking until a value that is deep equal to the specified val is received +// or the timeout is reached. +func await[T any](t testing.TB, dut *ondatra.DUTDevice, reqPath string, timeout time.Duration, awaitingVal T, valueExtractor func(*gpb.SubscribeResponse) T) { + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + req := &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Prefix: &gpb.Path{ + Target: dut.Name(), + }, + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: sPath.Elem, Origin: "openconfig"}, + Mode: gpb.SubscriptionMode_TARGET_DEFINED, + }}, + Mode: gpb.SubscriptionList_STREAM, + Encoding: gpb.Encoding_PROTO, + }, + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(req); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + // wait till received value is same as awaitingValue. + errCh := make(chan error) + defer close(errCh) + go func() { + for { + resp, err := subscribeClient.Recv() + if err != nil { + errCh <- err + return + } + if err := validateSubscribeUpdateResponse(resp); err != nil { + continue + } + val := valueExtractor(resp) + if reflect.DeepEqual(val, awaitingVal) { + errCh <- nil + return + } + } + }() + recvErr := <-errCh + if recvErr != nil { + t.Fatalf("await error : %v", recvErr) + } +} + +type Interface_FullyQualifiedInterfaceNamePath struct { + *ygnmi.NodePath + parent ygnmi.PathStruct +} + +type fullyQualifiedInterfaceNameKey struct { + dut *ondatra.DUTDevice + interfaceName string +} + +func FullyQualifiedInterfaceName(t *testing.T, dut *ondatra.DUTDevice, interfaceName string) string { + reqPath := fmt.Sprintf("/interfaces/interface[name=%s]/state/fully-qualified-interface-name", interfaceName) + fullyQualifiedInterfaceName := get(t, dut, reqPath, getResponseNotificationStringExtractor) + return fullyQualifiedInterfaceName +} + +func ReplaceFullyQualifiedInterfaceName(t *testing.T, dut *ondatra.DUTDevice, interfaceName string, value string) { + reqPath := fmt.Sprintf("/interfaces/interface[name=%s]/config/fully-qualified-interface-name", interfaceName) + replace(t, dut, reqPath, value) +} + +func AwaitFullyQualifiedInterfaceName(t *testing.T, dut *ondatra.DUTDevice, interfaceName string, timeout time.Duration, val string) { + reqPath := fmt.Sprintf("/interfaces/interface[name=%s]/state/fully-qualified-interface-name", interfaceName) + await[*string](t, dut, reqPath, timeout, &val, func(resp *gpb.SubscribeResponse) *string { + s := resp.Response.(*gpb.SubscribeResponse_Update).Update.Update[0].Val.Value.(*gpb.TypedValue_StringVal).StringVal + return &s + }) +} + +func GetLatestAvailableFirmwareVersion(t *testing.T, dut *ondatra.DUTDevice, xcvrName string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/transceiver/state/latest-available-firmware-version", xcvrName) + latestAvailableFirmwareVersion, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("%v", err) + return "" + } + return latestAvailableFirmwareVersion +} + +func GetFullyQualifiedName(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/state/fully-qualified-name", name) + fullyQualifiedName, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("%v", err) + return "" + } + return fullyQualifiedName +} + +func GetFullyQualifiedNameFromConfig(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/config/fully-qualified-name", name) + fullyQualifiedName, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("%v", err) + return "" + } + return fullyQualifiedName +} + +func ReplaceFullyQualifiedName(t *testing.T, dut *ondatra.DUTDevice, name string, value string) { + reqPath := fmt.Sprintf("/components/component[name=%s]/config/fully-qualified-name", name) + replace(t, dut, reqPath, value) +} + +func AwaitFullyQualifiedName(t *testing.T, dut *ondatra.DUTDevice, name string, timeout time.Duration, val string) { + reqPath := fmt.Sprintf("/components/component[name=%s]/state/fully-qualified-name", name) + await[*string](t, dut, reqPath, timeout, &val, func(resp *gpb.SubscribeResponse) *string { + s := resp.Response.(*gpb.SubscribeResponse_Update).Update.Update[0].Val.Value.(*gpb.TypedValue_StringVal).StringVal + return &s + }) +} + +func SensorType(t *testing.T, dut *ondatra.DUTDevice, ts *TemperatureSensorInfo) string { + if ts == nil { + t.Errorf("ts is nil") + return "" + } + reqPath := fmt.Sprintf("/components/component[name=%s]/sensor/state/sensor-type", ts.GetName()) + return get(t, dut, reqPath, getResponseNotificationStringExtractor) +} + +type E_Interface_HealthIndicator int64 + +const ( + Interface_HealthIndicator_UNSET E_Interface_HealthIndicator = 0 + Interface_HealthIndicator_GOOD E_Interface_HealthIndicator = 1 + Interface_HealthIndicator_BAD E_Interface_HealthIndicator = 2 +) + +func (h E_Interface_HealthIndicator) String() string { + if val, ok := globalEnumMap["E_Interface_HealthIndicator"][int64(h)]; ok { + return val.Name + } + return "" +} +func (h E_Interface_HealthIndicator) IsYANGGoEnum() {} +func (h E_Interface_HealthIndicator) ΛMap() map[string]map[int64]ygot.EnumDefinition { + return globalEnumMap +} + +func ReplaceHealthIndicator(t *testing.T, dut *ondatra.DUTDevice, name string, val E_Interface_HealthIndicator) { + value := val.String() + reqPath := fmt.Sprintf("/interfaces/interface[name=%s]/state/health-indicator", name) + replace(t, dut, reqPath, value) +} + +func AwaitHealthIndicator(t *testing.T, dut *ondatra.DUTDevice, name string, timeout time.Duration, val E_Interface_HealthIndicator) { + reqPath := fmt.Sprintf("/interfaces/interface[name=%s]/state/health-indicator", name) + strVal := val.String() + await[*string](t, dut, reqPath, timeout, &strVal, func(resp *gpb.SubscribeResponse) *string { + s := resp.Response.(*gpb.SubscribeResponse_Update).Update.Update[0].Val.Value.(*gpb.TypedValue_StringVal).StringVal + return &s + }) +} + +func StorageIOErrors(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) uint64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/io-errors", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint64Extractor) +} + +func StorageWriteAmplificationFactor(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) float64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/write-amplification-factor", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationDoubleExtractor) +} + +func StorageRawReadErrorRate(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) float64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/raw-read-error-rate", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationDoubleExtractor) +} + +func StorageThroughputPerformance(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) float64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/throughput-performance", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationDoubleExtractor) +} + +func StorageReallocatedSectorCount(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) uint64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/reallocated-sector-count", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint64Extractor) +} + +func StoragePowerOnSeconds(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) uint64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/power-on-seconds", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint64Extractor) +} + +func StorageSsdLifeLeft(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) uint64 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/ssd-life-left", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint64Extractor) +} + +func StorageAvgEraseCount(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) uint32 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/avg-erase-count", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint32Extractor) +} + +func StorageMaxEraseCount(t *testing.T, dut *ondatra.DUTDevice, s *StorageDeviceInfo) uint32 { + if s == nil { + t.Errorf("StorageDeviceInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/storage/state/max-erase-count", s.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint32Extractor) +} + +func FanSpeedControlPct(t *testing.T, dut *ondatra.DUTDevice, f *FanInfo) uint64 { + if f == nil { + t.Errorf("FanInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/fan/state/speed-control-pct", f.GetName()) + return get(t, dut, reqPath, getResponseNotificationUint64Extractor) +} + +func FPGAType(t *testing.T, dut *ondatra.DUTDevice, f *FPGAInfo) string { + if f == nil { + t.Errorf("FPGAInfo is nil") + return "" + } + reqPath := fmt.Sprintf("/components/component[name=%s]/state/type", f.GetName()) + return get(t, dut, reqPath, getResponseNotificationStringExtractor) +} + +func LookupComponentTypeOCCompliant(t *testing.T, dut *ondatra.DUTDevice, name string) (string, bool) { + reqPath := fmt.Sprintf("/components/component[name=%s]/state/type", name) + val, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + return "", false + } + + hardwareComponentTypes := oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_UNSET.ΛMap()["E_PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT"] + softwareComponentTypes := oc.PlatformTypes_OPENCONFIG_SOFTWARE_COMPONENT_UNSET.ΛMap()["E_PlatformTypes_OPENCONFIG_SOFTWARE_COMPONENT"] + for _, v := range hardwareComponentTypes { + if v.Name == val { + return val, true + } + } + for _, v := range softwareComponentTypes { + if v.Name == val { + return val, true + } + } + + return "", false +} + +type E_ResetCause_Cause int64 + +func (E_ResetCause_Cause) IsYANGGoEnum() {} +func (E_ResetCause_Cause) ΛMap() map[string]map[int64]ygot.EnumDefinition { return globalEnumMap } +func (e E_ResetCause_Cause) String() string { + if val, ok := globalEnumMap["E_ResetCause_Cause"][int64(e)]; ok { + return val.Name + } + return "" +} +func resetCauseFromString(cause string) E_ResetCause_Cause { + switch cause { + case "UNSET": + return ResetCause_Cause_UNSET + case "UNKNOWN": + return ResetCause_Cause_UNKNOWN + case "POWER": + return ResetCause_Cause_POWER + case "SWITCH": + return ResetCause_Cause_SWITCH + case "WATCHDOG": + return ResetCause_Cause_WATCHDOG + case "SOFTWARE": + return ResetCause_Cause_SOFTWARE + case "EMULATOR": + return ResetCause_Cause_EMULATOR + case "CPU": + return ResetCause_Cause_CPU + } + return ResetCause_Cause_UNKNOWN +} + +const ( + ResetCause_Cause_UNSET E_ResetCause_Cause = 0 + ResetCause_Cause_UNKNOWN E_ResetCause_Cause = 1 + ResetCause_Cause_POWER E_ResetCause_Cause = 2 + ResetCause_Cause_SWITCH E_ResetCause_Cause = 3 + ResetCause_Cause_WATCHDOG E_ResetCause_Cause = 4 + ResetCause_Cause_SOFTWARE E_ResetCause_Cause = 5 + ResetCause_Cause_EMULATOR E_ResetCause_Cause = 6 + ResetCause_Cause_CPU E_ResetCause_Cause = 7 +) + +type ResetCause struct { + index int + cause E_ResetCause_Cause +} + +func (r *ResetCause) GetIndex() int { + return r.index +} + +func (r *ResetCause) GetCause() E_ResetCause_Cause { + return r.cause +} + +func fpgaResetIndexImpl(t *testing.T, dut *ondatra.DUTDevice, fpgaName string, index int) (uint64, error) { + reqPath := fmt.Sprintf("/components/component[name=%s]/fpga/reset-causes/reset-cause[index=%v]/state/index", fpgaName, index) + return getWithError(t, dut, reqPath, getResponseNotificationUint64Extractor) +} + +func fpgaResetCauseImpl(t *testing.T, dut *ondatra.DUTDevice, fpgaName string, index int) (E_ResetCause_Cause, error) { + reqPath := fmt.Sprintf("/components/component[name=%s]/fpga/reset-causes/reset-cause[index=%v]/state/cause", fpgaName, index) + cause, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + return resetCauseFromString(cause), err +} + +func FPGAResetCauseMap(t *testing.T, dut *ondatra.DUTDevice, f *FPGAInfo) map[int]*ResetCause { + if f == nil { + t.Errorf("FPGAInfo is nil") + return nil + } + name := f.GetName() + resetCauses := map[int]*ResetCause{} + reqPath := fmt.Sprintf("/components/component[name=%s]/fpga/reset-causes/reset-cause", name) + lenCauses, err := getWithError(t, dut, reqPath, func(resp *gpb.GetResponse) int { + return len(resp.Notification[0].Update) + }) + if err != nil { + t.Errorf("%s not found", reqPath) + return nil + } + + // loop either till lenCauses or until an error is received. + for idx := 0; idx < lenCauses; idx++ { + index, err := fpgaResetIndexImpl(t, dut, name, idx) + if err != nil { + return resetCauses + } + cause, err := fpgaResetCauseImpl(t, dut, name, idx) + if err != nil { + return resetCauses + } + resetCauses[idx] = &ResetCause{index: int(index), cause: cause} + } + return resetCauses +} + +func FPGAResetCount(t *testing.T, dut *ondatra.DUTDevice, f *FPGAInfo) uint8 { + if f == nil { + t.Errorf("FPGAInfo is nil") + return 0 + } + reqPath := fmt.Sprintf("/components/component[name=%s]/fpga/state/reset-count", f.GetName()) + return uint8(get(t, dut, reqPath, getResponseNotificationUint64Extractor)) +} + +func FPGAResetCause(t *testing.T, dut *ondatra.DUTDevice, f *FPGAInfo, index int) E_ResetCause_Cause { + if f == nil { + t.Errorf("FPGAInfo is nil") + return 0 + } + cause, err := fpgaResetCauseImpl(t, dut, f.GetName(), index) + if err != nil { + t.Errorf("failed to fetch reset cause for %s/reset-causes/reset-cause[%v], err : ", f.GetName(), index, err) + return ResetCause_Cause_UNSET + } + return cause +} + +func EthernetPMD(t *testing.T, dut *ondatra.DUTDevice, xcvrName string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/transceiver/state/ethernet-pmd", xcvrName) + pmd, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return pmd +} + +func PortTransceiver(t *testing.T, dut *ondatra.DUTDevice, portName string) string { + reqPath := fmt.Sprintf("/interfaces/interface[name=%s]/state/transceiver", portName) + xcvrName, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return xcvrName +} + +// System_ConfigMetaDataPath represents the /openconfig-system/system/state/config-meta-data YANG schema element. +type System_ConfigMetaDataPath struct { + validateGoStruct + *ygnmi.NodePath + parent ygnmi.PathStruct +} + +func ConfigMetaData(n *system.SystemPath) *System_ConfigMetaDataPath { + ps := &System_ConfigMetaDataPath{ + NodePath: ygnmi.NewNodePath( + []string{"*", "config-meta-data"}, + map[string]interface{}{}, + n, + ), + parent: n, + } + return ps +} + +func SystemConfigMetaData(t *testing.T, dut *ondatra.DUTDevice) string { + reqPath := fmt.Sprintf("/system/state/config-meta-data") + metaData, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return metaData +} + +func SystemConfigMetaDataFromConfig(t *testing.T, dut *ondatra.DUTDevice) string { + reqPath := fmt.Sprintf("/system/config/config-meta-data") + metaData, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return metaData +} + +func ReplaceConfigMetaData(t *testing.T, dut *ondatra.DUTDevice, value string) { + reqPath := fmt.Sprintf("/system/config/config-meta-data") + replace(t, dut, reqPath, value) +} + +// FeatureLabel (list): List of feature labels. +// +// Defining module: "google-pins-system" +// Instantiating module: "openconfig-system" +// Path from parent: "feature-labels/feature-label" +// Path from root: "/system/feature-labels/feature-label" +// +// Label: uint32 +func SystemFeatureLabelPath(n *system.SystemPath, Label uint32) *System_FeatureLabelPath { + ps := &System_FeatureLabelPath{ + NodePath: ygnmi.NewNodePath( + []string{"feature-labels", "feature-label"}, + map[string]interface{}{"label": Label}, + n, + ), + } + return ps +} + +// System_FeatureLabelPath represents the /openconfig-system/system/feature-labels/feature-label YANG schema element. +type System_FeatureLabelPath struct { + *ygnmi.NodePath +} + +// System_FeatureLabel represents the /openconfig-system/system/feature-labels/feature-label YANG schema element. +type System_FeatureLabel struct { + Label *uint32 `path:"state/label|label" module:"google-pins-system/google-pins-system|google-pins-system" shadow-path:"config/label|label" shadow-module:"google-pins-system/google-pins-system|google-pins-system"` +} + +func (*System_FeatureLabel) IsYANGGoStruct() {} + +func (f *System_FeatureLabel) GetLabel() uint32 { + return *f.Label +} + +// Config returns a Query that can be used in gNMI operations. +// TODO: Kept ygnmi API as gnmi.Set doesn't work. +// For gnmi.Set to work, will have to add GO `json tag` parsing of `Label` to form the correct request. +func (n *System_FeatureLabelPath) Config() ygnmi.ConfigQuery[*System_FeatureLabel] { + return ygnmi.NewConfigQuery[*System_FeatureLabel]( + "System_FeatureLabel", + false, + true, + false, + false, + true, + false, + n, + nil, + nil, + func() *ytypes.Schema { + return &ytypes.Schema{ + Root: &oc.Root{}, + SchemaTree: oc.SchemaTree, + Unmarshal: oc.Unmarshal, + } + }, + nil, + nil, + ) +} + +// CreateFeatureLabel retrieves the value with the specified keys from +// the receiver System. If the entry does not exist, then it is created. +// It returns the existing or new list member. +func CreateFeatureLabel(label uint32) *System_FeatureLabel { + return &System_FeatureLabel{Label: &label} +} + +func AwaitSystemFeatureLabel(t *testing.T, dut *ondatra.DUTDevice, timeout time.Duration, val *System_FeatureLabel) { + reqPath := fmt.Sprintf("/system/feature-labels/feature-label[label=%d]/state", val.GetLabel()) + await[*System_FeatureLabel](t, dut, reqPath, timeout, val, func(resp *gpb.SubscribeResponse) *System_FeatureLabel { + l := uint32(resp.Response.(*gpb.SubscribeResponse_Update).Update.Update[0].Val.Value.(*gpb.TypedValue_UintVal).UintVal) + return &System_FeatureLabel{Label: &l} + }) +} + +func SystemFeatureLabel(t *testing.T, dut *ondatra.DUTDevice, label uint32) *System_FeatureLabel { + reqPath := fmt.Sprintf("/system/feature-labels/feature-label[label=%d]/state/label", label) + val, err := getWithError(t, dut, reqPath, getResponseNotificationUint32Extractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return nil + } + return CreateFeatureLabel(val) +} + +func SystemFeatureLabelFromConfig(t *testing.T, dut *ondatra.DUTDevice, label uint32) *System_FeatureLabel { + reqPath := fmt.Sprintf("/system/feature-labels/feature-label[label=%d]/config/label", label) + val, err := getWithError(t, dut, reqPath, getResponseNotificationUint32Extractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return nil + } + return CreateFeatureLabel(val) +} + +func SystemFeatureLabels(t *testing.T, dut *ondatra.DUTDevice) []*System_FeatureLabel { + reqPath := fmt.Sprintf("/system/feature-labels/feature-label") + + featureLabels, err := getWithError(t, dut, reqPath, func(resp *gpb.GetResponse) []uint32 { + exists := map[uint32]bool{} // getting duplicate labels from the request; keep a map to get unique values. + updates := resp.Notification[0].Update + var labels []uint32 + for idx, _ := range updates { + val := uint32(updates[idx].Val.GetUintVal()) + if _, found := exists[val]; found { + continue + } + labels = append(labels, val) + exists[val] = true + } + return labels + }) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return nil + } + + featureLabelsFromState := make([]*System_FeatureLabel, len(featureLabels)) + for idx, _ := range featureLabels { + s := SystemFeatureLabel(t, dut, featureLabels[idx]) + if s == nil { + return nil + } + featureLabelsFromState[idx] = s + } + + return featureLabelsFromState +} + +func ComponentStorageSide(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/state/storage-side", name) + storageSide, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return storageSide +} + +func ComponentChassisBaseMacAddress(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/chassis/state/base-mac-address", name) + baseMacAddress, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return baseMacAddress +} + +func ComponentChassisMacAddressPoolSize(t *testing.T, dut *ondatra.DUTDevice, name string) uint32 { + reqPath := fmt.Sprintf("/components/component[name=%s]/chassis/state/mac-address-pool-size", name) + macAddressPoolSize, err := getWithError(t, dut, reqPath, getResponseNotificationUint32Extractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return 0 + } + return macAddressPoolSize +} + +func ComponentChassisFullyQualifiedName(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/state/fully-qualified-name", name) + fqin, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return fqin +} + +func ComponentChassisPlatform(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/chassis/state/platform", name) + platform, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return platform +} + +func ComponentChassisModelName(t *testing.T, dut *ondatra.DUTDevice, name string) string { + reqPath := fmt.Sprintf("/components/component[name=%s]/chassis/state/model-name", name) + modelName, err := getWithError(t, dut, reqPath, getResponseNotificationStringExtractor) + if err != nil { + t.Errorf("fetching path %s failed with err : %v", reqPath, err) + return "" + } + return modelName +} + +func ReplaceComponentIntegratedCircuitNodeID(t *testing.T, dut *ondatra.DUTDevice, name string, val uint64) { + value := fmt.Sprintf("%v", val) + reqPath := fmt.Sprintf("/components/component[name=%s]/integrated-circuit/config/node-id", name) + replace(t, dut, reqPath, value) +} + +func UpdateLacpKey(t *testing.T, dut *ondatra.DUTDevice, interfaceName string, val uint16) { + reqPath := fmt.Sprintf("/lacp/interfaces/interface[name=%s]/config/lacp-key", interfaceName) + update(t, dut, reqPath, val) +} + +func AwaitLacpKey(t *testing.T, dut *ondatra.DUTDevice, interfaceName string, timeout time.Duration, val uint16) { + reqPath := fmt.Sprintf("/lacp/interfaces/interface[name=%s]/state/lacp-key", interfaceName) + await(t, dut, reqPath, timeout, &val, func(resp *gpb.SubscribeResponse) *uint16 { + s := uint16(resp.Response.(*gpb.SubscribeResponse_Update).Update.Update[0].Val.Value.(*gpb.TypedValue_UintVal).UintVal) + return &s + }) +} + +func GetConfig(t *testing.T, dut *ondatra.DUTDevice) []byte { + getReq := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{}, + Type: gpb.GetRequest_CONFIG, + Encoding: gpb.Encoding_JSON_IETF, + } + + ctx := context.Background() + // Fetch get client using the raw gNMI client. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("fetching gnmi client failed %v", err) + } + + getResp, err := gnmiClient.Get(ctx, getReq) + if err != nil { + t.Errorf("can't fetch the config.") + return nil + } + conf := getResp.Notification[0].Update[0].Val.GetJsonIetfVal() + + return []byte(conf) +} diff --git a/infrastructure/testhelper/gnmi.go b/infrastructure/testhelper/gnmi.go new file mode 100644 index 0000000..e9cc0e2 --- /dev/null +++ b/infrastructure/testhelper/gnmi.go @@ -0,0 +1,156 @@ +package testhelper + +import ( + "context" + "os" + "testing" + + closer "github.com/openconfig/gocloser" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc/system" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/pkg/errors" + "google.golang.org/grpc" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +) + +// Function pointers that interact with the switch. They enable unit testing +// of methods that interact with the switch. +var ( + gnmiSystemBootTimePath = func() *system.System_BootTimePath { + return gnmi.OC().System().BootTime() + } + gnmiSubscribeClientGet = func(t *testing.T, d *ondatra.DUTDevice, ctx context.Context, opts ...grpc.CallOption) (gpb.GNMI_SubscribeClient, error) { + c, err := d.RawAPIs().BindingDUT().DialGNMI(ctx) + if err != nil { + return nil, err + } + return c.Subscribe(ctx, opts...) + } + gnmiSet = func(t *testing.T, d *ondatra.DUTDevice, req *gpb.SetRequest) (*gpb.SetResponse, error) { + ctx := context.Background() + c, err := d.RawAPIs().BindingDUT().DialGNMI(ctx) + if err != nil { + return nil, err + } + return c.Set(ctx, req) + } +) + +// GNMIConfig provides an interface to implement config get. +type GNMIConfig interface { + ConfigGet() ([]byte, error) +} + +// GNMIConfigDUT contains the DUT for which the config get is being requested for. +type GNMIConfigDUT struct { + DUT *ondatra.DUTDevice +} + +// SubscribeRequestParams specifies the parameters that are used to create the +// SubscribeRequest. +// Target: The target to be specified in the prefix. +// Paths: List of paths to be added in the request. +// Mode: Subscription mode. +type SubscribeRequestParams struct { + Target string + Paths []ygnmi.PathStruct + Mode gpb.SubscriptionList_Mode +} + +// CreateSubscribeRequest creates SubscribeRequest message using the specified +// parameters that include the list of paths to be added in the request. +func CreateSubscribeRequest(params SubscribeRequestParams) (*gpb.SubscribeRequest, error) { + prefix := &gpb.Path{Origin: "openconfig"} + prefix.Target = params.Target + + var subscriptions []*gpb.Subscription + for _, path := range params.Paths { + resolvedPath, _, errs := ygnmi.ResolvePath(path) + if errs != nil { + return nil, errors.New("failed to resolve Openconfig path") + } + + subscription := &gpb.Subscription{ + Path: &gpb.Path{Elem: resolvedPath.Elem}, + } + subscriptions = append(subscriptions, subscription) + } + + return &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Prefix: prefix, + Subscription: subscriptions, + Mode: params.Mode, + Encoding: gpb.Encoding_PROTO, + }, + }, + }, nil +} + +// GNMIAble returns whether the gNMI server on the specified device is reachable +// or not. +func GNMIAble(t *testing.T, d *ondatra.DUTDevice) error { + // Since the Ondatra Get() API panics in case of failure, we need to use + // raw gNMI client to test reachability with the gNMI server on the switch. + // The gNMI server reachability is checked by fetching the system boot-time + // path from the switch. + params := SubscribeRequestParams{ + Target: testhelperDUTNameGet(d), + Paths: []ygnmi.PathStruct{gnmiSystemBootTimePath()}, + Mode: gpb.SubscriptionList_ONCE, + } + subscribeRequest, err := CreateSubscribeRequest(params) + if err != nil { + return errors.Wrapf(err, "failed to create SubscribeRequest") + } + + subscribeClient, err := gnmiSubscribeClientGet(t, d, context.Background()) + if err != nil { + return errors.Wrapf(err, "unable to get subscribe client") + } + defer closer.CloseAndLog(subscribeClient.CloseSend, "error closing gNMI send stream") + + if err := subscribeClient.Send(subscribeRequest); err != nil { + return errors.Wrapf(err, "failed to send gNMI subscribe request") + } + + if _, err := subscribeClient.Recv(); err != nil { + return errors.Wrapf(err, "subscribe client Recv() failed") + } + + return nil +} + +// ConfigGet returns a full config for the given DUT. +func (d GNMIConfigDUT) ConfigGet() ([]byte, error) { + return os.ReadFile("ondatra/data/config.json") +} + +// ConfigPush pushes the given config onto the DUT. If nil is passed in for config, +// this function will use ConfigGet() to get a full config for the DUT. +func ConfigPush(t *testing.T, dut *ondatra.DUTDevice, config *[]byte) error { + if dut == nil { + return errors.New("nil DUT passed into ConfigPush()") + } + if config == nil { + getConfig, err := GNMIConfigDUT{dut}.ConfigGet() + if err != nil { + return err + } + config = &getConfig + } + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: testhelperDUTNameGet(dut)}, + Replace: []*gpb.Update{{ + Path: &gpb.Path{}, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: *config}}, + }}, + } + t.Logf("Pushing config on %v: %v", testhelperDUTNameGet(dut), setRequest) + _, err := gnmiSet(t, dut, setRequest) + return err +} diff --git a/infrastructure/testhelper/gnoi.go b/infrastructure/testhelper/gnoi.go new file mode 100644 index 0000000..a87bfa9 --- /dev/null +++ b/infrastructure/testhelper/gnoi.go @@ -0,0 +1,167 @@ +package testhelper + +// This file contains helper method for gNOI services such as +// Reboot, Install etc. +import ( + "context" + "fmt" + "testing" + "time" + + log "github.com/golang/glog" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/pkg/errors" + + healthzpb "github.com/openconfig/gnoi/healthz" + syspb "github.com/openconfig/gnoi/system" +) + +// Function pointers that interact with the switch. They enable unit testing +// of methods that interact with the switch. +var ( + gnoiSystemClientGet = func(t *testing.T, d *ondatra.DUTDevice) syspb.SystemClient { + return d.RawAPIs().GNOI(t).System() + } + + gnoiHealthzClientGet = func(t *testing.T, d *ondatra.DUTDevice) healthzpb.HealthzClient { + return d.RawAPIs().GNOI(t).Healthz() + } + + gnmiSystemBootTimeGet = func(t *testing.T, d *ondatra.DUTDevice) uint64 { + return gnmi.Get(t, d, gnmi.OC().System().BootTime().State()) + } +) + +// RebootParams specify the reboot parameters used by the Reboot API. +type RebootParams struct { + request any + waitTime time.Duration + checkInterval time.Duration + lmTTkrID string // latency measurement testtracker UUID + lmTitle string // latency measurement title +} + +// NewRebootParams returns RebootParams structure with default values. +func NewRebootParams() *RebootParams { + return &RebootParams{ + waitTime: 4 * time.Minute, + checkInterval: 20 * time.Second, + } +} + +// WithWaitTime adds the period of time to wait for the reboot operation to be +// successful. +func (p *RebootParams) WithWaitTime(t time.Duration) *RebootParams { + p.waitTime = t + return p +} + +// WithCheckInterval adds the time interval to check whether the reboot +// operation has been successful. +func (p *RebootParams) WithCheckInterval(t time.Duration) *RebootParams { + p.checkInterval = t + return p +} + +// WithRequest adds the reboot request in RebootParams. The reboot request can +// be one of the following: +// 1) RebootMethod such as syspb.RebootMethod_COLD. +// 2) RebootRequest protobuf. +func (p *RebootParams) WithRequest(r any) *RebootParams { + p.request = r + return p +} + +// WithLatencyMeasurement adds testtracker uuid and title for latency measurement. +func (p *RebootParams) WithLatencyMeasurement(testTrackerID, title string) *RebootParams { + p.lmTTkrID = testTrackerID + p.lmTitle = title + return p +} + +// measureLatency returns true if latency measurement parameters are set and valid. +func (p *RebootParams) measureLatency() bool { + return p.waitTime > 0 && p.lmTitle != "" +} + +// Reboot sends a RebootRequest message to the switch. It waits for a specified +// amount of time for the switch reboot to be successful. A switch reboot is +// considered to be successful if the gNOI server is up and the boot time is +// after the reboot request time. +func Reboot(t *testing.T, d *ondatra.DUTDevice, params *RebootParams) error { + if params.waitTime < params.checkInterval { + return errors.Errorf("wait time:%v cannot be less than check interval:%v", params.waitTime, params.checkInterval) + } + + var req *syspb.RebootRequest + switch v := params.request.(type) { + case syspb.RebootMethod: + // User only specified the reboot type. Construct reboot request. + req = &syspb.RebootRequest{ + Method: v, + Message: "Reboot", + } + case *syspb.RebootRequest: + // Use the specified reboot request. + req = v + default: + return errors.New("invalid reboot request (valid parameters are RebootRequest protobuf and RebootMethod)") + } + + log.Infof("Rebooting %v switch", testhelperDUTNameGet(d)) + timeBeforeReboot := time.Now().UnixNano() + systemClient := gnoiSystemClientGet(t, d) + + if _, err := systemClient.Reboot(context.Background(), req); err != nil { + return errors.Wrapf(err, "reboot RPC failed") + } + + if params.waitTime == 0 { + // User did not request a wait time which implies that the API did not verify whether + // the switch has rebooted or not. Therefore, do not return an error in this case. + return nil + } + + log.Infof("Polling gNOI server reachability in %v intervals for max duration of %v", params.checkInterval, params.waitTime) + for timeout := time.Now().Add(params.waitTime); time.Now().Before(timeout); { + // The switch backend might not have processed the request or might take + // sometime to execute the request. So wait for check interval time and + // later verify that the switch rebooted within the specified wait time. + time.Sleep(params.checkInterval) + doneTime := time.Now() + timeElapsed := (doneTime.UnixNano() - timeBeforeReboot) / int64(time.Second) + + if err := GNOIAble(t, d); err != nil { + log.Infof("gNOI server not up after %v seconds", timeElapsed) + continue + } + log.Infof("gNOI server up after %v seconds", timeElapsed) + + // An extra check to ensure that the system has rebooted. + if bootTime := gnmiSystemBootTimeGet(t, d); bootTime < uint64(timeBeforeReboot) { + log.Infof("Switch has not rebooted after %v seconds", timeElapsed) + continue + } + + log.Infof("Switch rebooted after %v seconds", timeElapsed) + return nil + } + + err := errors.Errorf("failed to reboot %v", testhelperDUTNameGet(d)) + + return err +} + +// GNOIAble returns whether the gNOI server on the specified device is reachable +// or not. +func GNOIAble(t *testing.T, d *ondatra.DUTDevice) error { + // Time() gNOI request is used to verify the gNOI server reachability. + _, err := gnoiSystemClientGet(t, d).Time(context.Background(), &syspb.TimeRequest{}) + return err +} + +// HealthzGetPortDebugData returns port debug data given an interface. +func HealthzGetPortDebugData(t *testing.T, d *ondatra.DUTDevice, intfName string) error { + return fmt.Errorf("unimplemented method HealthzGetPortDebugData") +} diff --git a/infrastructure/testhelper/lacp.go b/infrastructure/testhelper/lacp.go new file mode 100644 index 0000000..e5405ac --- /dev/null +++ b/infrastructure/testhelper/lacp.go @@ -0,0 +1,129 @@ +package testhelper + +import ( + "testing" + "time" + + log "github.com/golang/glog" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/pkg/errors" +) + +// PeerPorts holds the name of 2 Ethernet interfaces. These interfaces will be on separate machines, +// but connected to each other by a cable. +type PeerPorts struct { + Host string + Peer string +} + +// PeerPortsBySpeed iterates through all the available Ethernet ports on a host device, and +// determines if they have a matching port on the peer device. All host ports with a valid peer will +// be grouped together based on their speed. +func PeerPortsBySpeed(t *testing.T, host *ondatra.DUTDevice, peer *ondatra.DUTDevice) map[oc.E_IfEthernet_ETHERNET_SPEED][]PeerPorts { + peerPortsBySpeed := make(map[oc.E_IfEthernet_ETHERNET_SPEED][]PeerPorts) + + for _, hostPort := range testhelperDUTPortsGet(host) { + peerPort := testhelperDUTPortGet(t, peer, testhelperOndatraPortIDGet(hostPort)) + + // Verify the host port is UP. Otherwise, LACPDU packets will never be transmitted. + if got, want := testhelperIntfOperStatusGet(t, host, testhelperOndatraPortNameGet(hostPort)), oc.Interface_OperStatus_UP; got != want { + log.Warningf("Port %v:%v oper state will not work for LACP testing: want=%v got=%v", testhelperDUTNameGet(host), testhelperOndatraPortNameGet(hostPort), want, got) + continue + } + + // Verify we are not already part of an existing PortChannel since one port cannot belong to + // multiple PortChannels. + if got, want := testhelperConfigIntfAggregateIDGet(t, host, testhelperOndatraPortNameGet(hostPort)), ""; got != want { + log.Warningf("Port %v:%v cannot be used since it is already assigned to a PortChannel: want=%v got=%v", testhelperDUTNameGet(host), testhelperOndatraPortNameGet(hostPort), want, got) + continue + } + + // Check that the host port has a valid peer port. + if peerPort == nil { + log.Warningf("Port %v:%v does not have a peer on %v.", testhelperDUTNameGet(host), testhelperOndatraPortNameGet(hostPort), testhelperDUTNameGet(peer)) + continue + } + + log.Infof("Found peer ports: %v:%v and %v:%v", testhelperDUTNameGet(host), testhelperOndatraPortNameGet(hostPort), testhelperDUTNameGet(peer), testhelperOndatraPortNameGet(peerPort)) + portSpeed := testhelperConfigPortSpeedGet(t, host, testhelperOndatraPortNameGet(hostPort)) + peerPortsBySpeed[portSpeed] = append(peerPortsBySpeed[portSpeed], PeerPorts{testhelperOndatraPortNameGet(hostPort), testhelperOndatraPortNameGet(peerPort)}) + } + + return peerPortsBySpeed +} + +// PeerPortGroupWithNumMembers returns a list of PeerPorts of size `numMembers`. +func PeerPortGroupWithNumMembers(t *testing.T, host *ondatra.DUTDevice, peer *ondatra.DUTDevice, numMembers int) ([]PeerPorts, error) { + // GPINs requires that all members of a LACP LAG have the same speed. So we first group all the + // ports based on their configured speed. + peerPortsBySpeed := PeerPortsBySpeed(t, host, peer) + + // Then we search through the port speed gropus for one that has enough members to match the users + // requested amount. + for _, ports := range peerPortsBySpeed { + if len(ports) >= numMembers { + // Only return enough members to match the users request. + return ports[0:numMembers], nil + } + } + return nil, errors.Errorf("cannot make group of %v member ports with the same speed from %v.", numMembers, peerPortsBySpeed) +} + +// GeneratePortChannelInterface will return a minimal PortChannel interface that tests can extend as needed. +func GeneratePortChannelInterface(portChannelName string) oc.Interface { + enabled := true + + description := "PortChannel: " + portChannelName + " used for testing gNMI configuration." + minLinks := uint16(1) + + // Unsupported fields: Id, Aggregation/LagType + return oc.Interface{ + Name: &portChannelName, + Enabled: &enabled, + Type: oc.IETFInterfaces_InterfaceType_ieee8023adLag, + Description: &description, + Aggregation: &oc.Interface_Aggregation{ + LagType: oc.IfAggregate_AggregationType_LACP, + MinLinks: &minLinks, + }, + } +} + +// GenerateLACPInterface creates a minimal LACP interface that tests can then extend as needed. +func GenerateLACPInterface(pcName string) oc.Lacp_Interface { + + return oc.Lacp_Interface{ + Name: &pcName, + Interval: oc.Lacp_LacpPeriodType_FAST, + LacpMode: oc.Lacp_LacpActivityType_ACTIVE, + } +} + +// RemovePortChannelFromDevice will cleanup all configs relating to a PortChannel on a given switch. +// It will also verify that the state has been updated before returning. If the state fails to +// converge then an error will be returned. +func RemovePortChannelFromDevice(t *testing.T, timeout time.Duration, dut *ondatra.DUTDevice, portChannelName string) error { + // We only need to delete the PortChannel interface and gNMI should take care of all the other + // configs relating to the PortChannel. + testhelperIntfDelete(t, dut, portChannelName) + + stopTime := time.Now().Add(timeout) + for time.Now().Before(stopTime) { + if !testhelperIntfLookup(t, dut, portChannelName).IsPresent() { + return nil + } + time.Sleep(time.Second) + } + + return errors.Errorf("interface still exists after %v", timeout) +} + +// AssignPortsToAggregateID will assign the list of ports to the given aggregate ID on a device. The +// aggregate ID should correspond to an existing PortChannel interface or this call will fail. +func AssignPortsToAggregateID(t *testing.T, dut *ondatra.DUTDevice, portChannelName string, portNames ...string) { + for _, portName := range portNames { + log.Infof("Assigning %v:%v to %v", testhelperDUTNameGet(dut), portName, portChannelName) + testhelperIntfAggregateIDReplace(t, dut, portName, portChannelName) + } +} diff --git a/infrastructure/testhelper/p4rt.go b/infrastructure/testhelper/p4rt.go new file mode 100644 index 0000000..fc723d8 --- /dev/null +++ b/infrastructure/testhelper/p4rt.go @@ -0,0 +1,305 @@ +package testhelper + +// This file provides helper APIs to perform P4RT related operations. + +import ( + "context" + "encoding/hex" + "fmt" + "os" + "strconv" + "testing" + "time" + + log "github.com/golang/glog" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/encoding/prototext" + + p4infopb "github.com/p4lang/p4runtime/go/p4/config/v1" + p4pb "github.com/p4lang/p4runtime/go/p4/v1" +) + +var ( + icName = "integrated_circuit0" + defaultDeviceID uint64 = 183934027 + + testhelperPortIDGet = func(t *testing.T, d *ondatra.DUTDevice, port string) (int, error) { + idInfo, present := gnmi.Lookup(t, d, gnmi.OC().Interface(port).Id().State()).Val() + if present { + return int(idInfo), nil + } + return 0, errors.Errorf("failed to get port ID for port %v from switch", port) + } + testhelperDeviceIDGet = func(t *testing.T, d *ondatra.DUTDevice) (uint64, error) { + deviceInfo, present := gnmi.Lookup(t, d, gnmi.OC().Component(icName).IntegratedCircuit().State()).Val() + if present && deviceInfo.NodeId != nil { + return *deviceInfo.NodeId, nil + } + // Configure default device ID on the switch. + gnmi.Replace(t, d, gnmi.OC().Component(icName).IntegratedCircuit().NodeId().Config(), defaultDeviceID) + // Verify that default device ID has been configured and return that. + if got, want := gnmi.Get(t, d, gnmi.OC().Component(icName).IntegratedCircuit().NodeId().State()), defaultDeviceID; got != want { + return 0, errors.Errorf("failed to configure default device ID") + } + return defaultDeviceID, nil + } +) + +// PacketOut structure enables the user to specify the following information for +// performing packet-out operation on the switch: +// EgressPort: Front panel port from which the packet needs to be sent out. +// Count: Number of packets to be egressed. +// Interval: Time interval between successive packet-out operations. +// Packet: Raw packet to be sent out. +type PacketOut struct { + SubmitToIngress bool + EgressPort string + Count uint + Interval time.Duration + Packet []byte +} + +// P4RTClient wraps P4RuntimeClient and implements methods for performing P4RT +// operations. +type P4RTClient struct { + client p4pb.P4RuntimeClient + stream p4pb.P4Runtime_StreamChannelClient + deviceID uint64 + electionID *p4pb.Uint128 + isMaster bool + dut *ondatra.DUTDevice + p4Info *p4infopb.P4Info +} + +// P4RTClientOptions contains the fields for creation of P4RTClient. +type P4RTClientOptions struct { + p4info *p4infopb.P4Info +} + +func generateElectionID() *p4pb.Uint128 { + // Get time in milliseconds. + t := uint64(time.Now().UnixNano() / 1000000) + return &p4pb.Uint128{ + Low: t % 1000, + High: t / 1000, + } +} + +// SetMastership tries to configure P4RT client as master by sending master +// arbitration request to the switch. +func (p *P4RTClient) SetMastership() error { + // Don't take any action if the client is already the master. + if p.isMaster { + return nil + } + + mastershipReq := &p4pb.StreamMessageRequest{ + Update: &p4pb.StreamMessageRequest_Arbitration{ + Arbitration: &p4pb.MasterArbitrationUpdate{ + DeviceId: p.deviceID, + ElectionId: p.electionID, + }, + }, + } + + log.Infof("Sending master arbitration request with DeviceId:%v, ElectionId:%v", p.deviceID, p.electionID) + if err := p.stream.Send(mastershipReq); err != nil { + return errors.Wrapf(err, "master arbitration send request failed") + } + + res, err := p.stream.Recv() + if err != nil { + return errors.Wrapf(err, "stream Recv() error") + } + + arb := res.GetArbitration() + if arb == nil { + return errors.Errorf("unexpected response received from switch: %v", res.String()) + } + if codes.Code(arb.Status.Code) != codes.OK { + return errors.Errorf("master arbitration failed (response status: %v)", arb.Status) + } + + log.Infof("Master arbitration successful: client is master") + p.isMaster = true + return nil +} + +// P4InfoDetails is an interface to get P4Info of a chassis. +type P4InfoDetails interface { + P4Info() (*p4infopb.P4Info, error) +} + +// P4Info gets P4Info of the switch. +func (p *P4RTClient) P4Info() (*p4infopb.P4Info, error) { + var p4Info *p4infopb.P4Info + err := fmt.Errorf("P4Info is not implemented") + + // Read P4Info from file. + p4Info = &p4infopb.P4Info{} + data, err := os.ReadFile("ondatra/data/p4rtconfig.prototext") + if err != nil { + return nil, err + } + err = prototext.Unmarshal(data, p4Info) + + return p4Info, err +} + +// FetchP4Info fetches P4Info from the switch. +func (p *P4RTClient) FetchP4Info() (*p4infopb.P4Info, error) { + req := &p4pb.GetForwardingPipelineConfigRequest{DeviceId: p.deviceID} + resp, err := p.client.GetForwardingPipelineConfig(context.Background(), req) + if err != nil { + return nil, errors.Wrap(err, "GetForwardingPipelineConfig() failed") + } + if resp == nil { + return nil, errors.New("received nil GetForwardingPipelineConfigResponse") + } + config := resp.GetConfig() + if config == nil { + return nil, nil + } + return config.GetP4Info(), nil +} + +// PushP4Info pushes P4Info into the switch. +func (p *P4RTClient) PushP4Info() error { + var err error + if p.p4Info == nil { + p.p4Info, err = p.P4Info() + if err != nil { + return errors.Wrapf(err, "failed to fetch P4Info") + } + } + config := &p4pb.ForwardingPipelineConfig{ + P4Info: p.p4Info, + } + req := &p4pb.SetForwardingPipelineConfigRequest{ + DeviceId: p.deviceID, + ElectionId: p.electionID, + Action: p4pb.SetForwardingPipelineConfigRequest_RECONCILE_AND_COMMIT, + Config: config, + } + + _, err = p.client.SetForwardingPipelineConfig(context.Background(), req) + if err != nil { + return errors.Wrapf(err, "SetForwardingPipelineConfig operation failed") + } + + log.Infof("P4Info push successful") + return nil +} + +// FetchP4RTClient method fetches P4RTClient associated with a device. If the +// client does not exist, then it creates one and caches it for future use. +// During client creation, it performs master arbitration and P4Info push. +func FetchP4RTClient(t *testing.T, d *ondatra.DUTDevice, p p4pb.P4RuntimeClient, options *P4RTClientOptions) (*P4RTClient, error) { + p4Client := &P4RTClient{ + client: p, + dut: d, + } + if options != nil { + p4Client.p4Info = options.p4info + } + var err error + p4Client.deviceID, err = testhelperDeviceIDGet(t, d) + if err != nil { + return nil, err + } + // Create stream for master arbitration and packet I/O. + var streamErr error + p4Client.stream, streamErr = p4Client.client.StreamChannel(context.Background()) + if streamErr != nil { + return nil, errors.Wrap(streamErr, "failed to create stream for master arbitration") + } + + // Configure P4RT client as master. + p4Client.electionID = generateElectionID() + if err := p4Client.SetMastership(); err != nil { + return nil, errors.Wrap(err, "failed to configure P4RT client as master") + } + + // Push P4Info only if it isn't present in the switch. + p4Info, err := p4Client.FetchP4Info() + if err != nil { + return nil, errors.Wrap(err, "FetchP4Info() failed") + } + if p4Info == nil { + if err := p4Client.PushP4Info(); err != nil { + return nil, errors.Wrap(err, "P4Info push failed") + } + } + + return p4Client, nil +} + +// SendPacketOut instructs the P4RT server on the switch to perform packet-out +// operation. +func (p *P4RTClient) SendPacketOut(t *testing.T, packetOut *PacketOut) error { + // Validate user input parameters. + if packetOut.SubmitToIngress && packetOut.EgressPort != "" { + return errors.Errorf("cannot have both SubmitToIngress and EgressPort set in the packet-out request: %+v", packetOut) + } + + // Metadata value cannot be empty, so dummy value is set and ignored when SubmitToIngress is true. + portID := "Unused" + submitToIngress := []byte{0} + if packetOut.SubmitToIngress { + submitToIngress = []byte{1} + } else { + egressPortID, err := testhelperPortIDGet(t, p.dut, packetOut.EgressPort) + if err != nil { + return errors.Errorf("failed to get ID for port %v: %v", packetOut.EgressPort, err) + } + portID = strconv.Itoa(egressPortID) + } + + if packetOut.Count == 0 { + return errors.Errorf("packet-out count should be > 0 in packet-out request: %+v", packetOut) + } + count := packetOut.Count + interval := packetOut.Interval + + // Prepare packet I/O request. + pktOut := &p4pb.PacketOut{ + Payload: packetOut.Packet, + } + // Add egress_port metadata. + pktOut.Metadata = append(pktOut.Metadata, &p4pb.PacketMetadata{ + MetadataId: 1, + Value: []byte(portID), + }) + // Add submit_to_ingress metadata. + pktOut.Metadata = append(pktOut.Metadata, &p4pb.PacketMetadata{ + MetadataId: 2, + Value: submitToIngress, + }) + // Add unused_pad metadata. + pktOut.Metadata = append(pktOut.Metadata, &p4pb.PacketMetadata{ + MetadataId: 3, + Value: []byte{0}, + }) + packetOutReq := &p4pb.StreamMessageRequest{ + Update: &p4pb.StreamMessageRequest_Packet{Packet: pktOut}, + } + + log.Infof("Sending %v packets to the switch at %v interval. Packet:\n%v", count, interval, hex.Dump(packetOut.Packet)) + for c := uint(1); c <= count; c++ { + // Send packet-out request to the switch. + if err := p.stream.Send(packetOutReq); err != nil { + return errors.Errorf("Packet-out request failed for packet number: %v (%v)", c, err) + } + // Sleep only if user has specified time interval and more packets need to be sent. + if interval > 0 && c < count { + time.Sleep(interval) + } + } + + log.Infof("Packet-out operation completed") + return nil +} diff --git a/infrastructure/testhelper/platform_components.go b/infrastructure/testhelper/platform_components.go new file mode 100644 index 0000000..fbf4c71 --- /dev/null +++ b/infrastructure/testhelper/platform_components.go @@ -0,0 +1,460 @@ +package testhelper + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/pkg/errors" +) + +// Software Component APIs. + +// SwitchNameRegex returns the regex for switch name. +func SwitchNameRegex() string { + return "" +} + +// ImageVersionRegex returns the regular expressions for the image version of the switch. +func ImageVersionRegex() []string { + return []string{ + "^pins_daily_(20\\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])_([0-1]?[0-9]|2[0-3])_RC(\\d{2})$", + "^pins_release_(20\\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])_([0-1]?[0-9]|2[0-3])_(prod|dev)_RC(\\d{2})$", + } +} + +// System APIs. + +// GetIndex returns the CPU index. +func (c CPUInfo) GetIndex() uint32 { + return c.Index +} + +// GetMaxAverageUsage returns the maximum CPU average usage. +func (c CPUInfo) GetMaxAverageUsage() uint8 { + return c.MaxAverageUsage +} + +// RebootTimeForDevice returns the maximum time that the device might take to reboot. +func RebootTimeForDevice(t *testing.T, d *ondatra.DUTDevice) (time.Duration, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return 0, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.SystemInfo.RebootTime, nil +} + +// LoggingServerAddressesForDevice returns remote logging server address information for a platform. +func LoggingServerAddressesForDevice(t *testing.T, d *ondatra.DUTDevice) (LoggingInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return LoggingInfo{}, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.SystemInfo.LoggingInfo, nil +} + +// CPUInfoForDevice returns CPU related information for a device. +func CPUInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]CPUInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.SystemInfo.CPUInfo, nil +} + +// GetPhysical returns the expected physical memory. +func (m MemoryInfo) GetPhysical() uint64 { + return m.Physical +} + +// GetFreeThreshold returns the free memory threshold. +func (m MemoryInfo) GetFreeThreshold() uint64 { + return m.FreeThreshold +} + +// GetUsedThreshold returns the used memory threshold. +func (m MemoryInfo) GetUsedThreshold() uint64 { + return m.UsedThreshold +} + +// GetCorrectableEccErrorThreshold returns the correctable ECC error threshold. +func (m MemoryInfo) GetCorrectableEccErrorThreshold() uint64 { + return m.CorrectableEccErrorThreshold +} + +// MemoryInfoForDevice returns memory related information for a device. +func MemoryInfoForDevice(t *testing.T, d *ondatra.DUTDevice) (MemoryInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return MemoryInfo{}, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.SystemInfo.MemInfo, nil +} + +// GetName returns the name of the mount point. +func (m MountPointInfo) GetName() string { + return m.Name +} + +// MountPointsInfoForDevice returns information about all "required" +// mount points for a device. +func MountPointsInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]MountPointInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.SystemInfo.MountPointInfo, nil +} + +// GetIPv4Address returns NTP server's IPv4 addresses. +func (n NTPServerInfo) GetIPv4Address() []string { + return n.IPv4Address +} + +// GetIPv6Address returns NTP server's IPv6 addresses. +func (n NTPServerInfo) GetIPv6Address() []string { + return n.IPv6Address +} + +// GetStratumThreshold returns the stratum threshold for the NTP server. +func (n NTPServerInfo) GetStratumThreshold() uint8 { + return n.StratumThreshold +} + +// NTPServerInfoForDevice returns NTP server related information for a device. +func NTPServerInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]NTPServerInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.SystemInfo.NTPServerInfo, nil +} + +// Integrated Circuit APIs. + +// GetName returns the integrated-circuit name. +func (i IntegratedCircuitInfo) GetName() string { + return i.Name +} + +// GetCorrectedParityErrorsThreshold returns the corrected-parity-error +// threshold for the integrated-circuit. +func (i IntegratedCircuitInfo) GetCorrectedParityErrorsThreshold() uint64 { + return i.CorrectedParityErrorsThreshold +} + +// ICInfoForDevice returns integrated-circuit related information for all +// integrated circuits present in a platform. +func ICInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]IntegratedCircuitInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.HardwareInfo.ICs, nil +} + +// FPGA APIs. + +// GetName returns the FPGA name. +func (f FPGAInfo) GetName() string { + return f.Name +} + +// GetMfgName returns the FPGA manufacturer. +func (f FPGAInfo) GetMfgName() string { + return f.Manufacturer +} + +// GetDescription returns the FPGA description. +func (f FPGAInfo) GetDescription() string { + return f.Description +} + +// GetFirmwareVersionRegex returns the FPGA firmware version regex. +func (f FPGAInfo) GetFirmwareVersionRegex() string { + return f.FirmwareVersionRegex +} + +// GetResetCauseNum returns the number of reset causes reported by the FPGA. +func (f FPGAInfo) GetResetCauseNum() int { + return f.ResetCauseNum +} + +// FPGAInfoForDevice returns FPGA related information for all FPGAs present in a +// platform. +func FPGAInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]FPGAInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.HardwareInfo.FPGAs, nil +} + +// GetMin returns the minimum threshold for the power information. +func (p Threshold32) GetMin() float32 { + return p.Min +} + +// GetMax returns the maximum threshold for the power information. +func (p Threshold32) GetMax() float32 { + return p.Max +} + +// GetMin returns the minimum threshold for the power information. +func (p Threshold64) GetMin() float64 { + return p.Min +} + +// GetMax returns the maximum threshold for the power information. +func (p Threshold64) GetMax() float64 { + return p.Max +} + +// TemperatureSensorType defines the type of temperature sensors. +type TemperatureSensorType int + +// Type of temperature sensors. +const ( + CPUTempSensor TemperatureSensorType = iota + HeatsinkTempSensor + ExhaustTempSensor + InletTempSensor + DimmTempSensor +) + +// GetName returns the temperature sensor name. +func (t TemperatureSensorInfo) GetName() string { + return t.Name +} + +// GetLocation returns the temperature sensor location. +func (t TemperatureSensorInfo) GetLocation() string { + return t.Location +} + +// GetMaxTemperature returns the temperature threshold for the temperature sensor. +func (t TemperatureSensorInfo) GetMaxTemperature() float64 { + return t.MaxTemperature +} + +// TemperatureSensorInfoForDevice returns information about all temperature sensors +// of the specified type. +func TemperatureSensorInfoForDevice(t *testing.T, d *ondatra.DUTDevice, s TemperatureSensorType) ([]TemperatureSensorInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + + switch s { + case CPUTempSensor: + return info.HardwareInfo.CPU, nil + case HeatsinkTempSensor: + return info.HardwareInfo.Heatsink, nil + case ExhaustTempSensor: + return info.HardwareInfo.Exhaust, nil + case InletTempSensor: + return info.HardwareInfo.Inlet, nil + case DimmTempSensor: + return info.HardwareInfo.Dimm, nil + } + + return nil, errors.Errorf("invalid sensor type: %v", s) +} + +// GetName returns the security component name. +func (s SecurityComponentInfo) GetName() string { + return s.Name +} + +// SecurityInfoForDevice returns information about all security components. +func SecurityInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]SecurityComponentInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + + return info.HardwareInfo.Security, nil +} + +// IsValid checks if a value is in the thresholds. +func (t Thresholds[T]) IsValid(v T) bool { + if t.HasLo && v < t.Lo { + return false + } + if t.HasHi && v > t.Hi { + return false + } + return true +} + +// ThresholdsToString is a helper method to convert a set of thresholds to a readable string. +func (t Thresholds[T]) String() string { + var sb strings.Builder + if t.HasLo { + sb.WriteString("lo:>=") + sb.WriteString(fmt.Sprintf("%v", t.Lo)) + } else { + sb.WriteString("(no lo)") + } + sb.WriteString(" ") + + if t.HasHi { + sb.WriteString("hi:<=") + sb.WriteString(fmt.Sprintf("%v", t.Hi)) + } else { + sb.WriteString("(no hi)") + } + + return sb.String() +} + +// GetWriteAmplificationFactorThresholds returns the write amplification factor thresholds. +func (s SmartDataInfo) GetWriteAmplificationFactorThresholds() Thresholds[float64] { + return s.WriteAmplificationFactorThresholds +} + +// GetRawReadErrorRateThresholds returns the raw read error rate thresholds. +func (s SmartDataInfo) GetRawReadErrorRateThresholds() Thresholds[float64] { + return s.RawReadErrorRateThresholds +} + +// GetThroughputPerformanceThresholds returns the throughput performance thresholds. +func (s SmartDataInfo) GetThroughputPerformanceThresholds() Thresholds[float64] { + return s.ThroughputPerformanceThresholds +} + +// GetReallocatedSectorCountThresholds returns the throughput performance thresholds. +func (s SmartDataInfo) GetReallocatedSectorCountThresholds() Thresholds[uint64] { + return s.ReallocatedSectorCountThresholds +} + +// GetPowerOnSecondsThresholds returns the throughput performance thresholds. +func (s SmartDataInfo) GetPowerOnSecondsThresholds() Thresholds[uint64] { + return s.PowerOnSecondsThresholds +} + +// GetSsdLifeLeftThresholds returns the SSD life left thresholds. +func (s SmartDataInfo) GetSsdLifeLeftThresholds() Thresholds[uint64] { + return s.SSDLifeLeftThresholds +} + +// GetAvgEraseCountThresholds returns the average erase count thresholds. +func (s SmartDataInfo) GetAvgEraseCountThresholds() Thresholds[uint32] { + return s.AvgEraseCountThresholds +} + +// GetMaxEraseCountThresholds returns the average erase count thresholds. +func (s SmartDataInfo) GetMaxEraseCountThresholds() Thresholds[uint32] { + return s.MaxEraseCountThresholds +} + +// GetName returns the storage device name. +func (s StorageDeviceInfo) GetName() string { + return s.Name +} + +// GetIsRemovable returns whether the storage device is removable or not. +func (s StorageDeviceInfo) GetIsRemovable() bool { + return s.IsRemovable +} + +// GetIoErrorsThreshold returns the threshold for storage device I/O errors. +func (s StorageDeviceInfo) GetIoErrorsThreshold() uint64 { + return s.IOErrorsThreshold +} + +// GetSmartDataInfo returns the SMART data info. +func (s StorageDeviceInfo) GetSmartDataInfo() SmartDataInfo { + return s.SmartDataInfo +} + +// StorageDeviceInfoForDevice returns information about all storage devices. +func StorageDeviceInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]StorageDeviceInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + + return info.HardwareInfo.Storage, nil +} + +// GetName returns the fan name. +func (f FanInfo) GetName() string { + return f.Name +} + +// GetIsRemovable returns whether the fan is removable or not. +func (f FanInfo) GetIsRemovable() bool { + return f.IsRemovable +} + +// GetLocation returns the location of the fan. +func (f FanInfo) GetLocation() string { + return f.Location +} + +// GetMaxSpeed returns the maximum speed of the fan. +func (f FanInfo) GetMaxSpeed() uint32 { + return f.MaxSpeed +} + +// GetParent returns the parent component of the fan. +func (f FanInfo) GetParent() string { + return f.Parent +} + +// GetName returns the fan tray name. +func (f FanTrayInfo) GetName() string { + return f.Name +} + +// GetIsRemovable returns whether the fan tray is removable or not. +func (f FanTrayInfo) GetIsRemovable() bool { + return f.IsRemovable +} + +// GetParent returns the parent component of the fan tray. +func (f FanTrayInfo) GetParent() string { + return f.Parent +} + +// GetLocation returns the location of the fan tray. +func (f FanTrayInfo) GetLocation() string { + return f.Location +} + +// FanInfoForDevice returns information about all fans. +func FanInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]FanInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + + return info.HardwareInfo.Fans, nil +} + +// FanTrayInfoForDevice returns information about all fan trays. +func FanTrayInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]FanTrayInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + + return info.HardwareInfo.Fantrays, nil +} + +// GetName returns the PCIe device name. +func (p PCIeInfo) GetName() string { + return p.Name +} + +// PcieInfoForDevice returns information about all PCIe devices. +func PcieInfoForDevice(t *testing.T, d *ondatra.DUTDevice) ([]PCIeInfo, error) { + info, err := platformInfoForDevice(t, d) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch platform specific information") + } + return info.HardwareInfo.PCIe, nil +} diff --git a/infrastructure/testhelper/platform_info.go b/infrastructure/testhelper/platform_info.go new file mode 100644 index 0000000..57345ae --- /dev/null +++ b/infrastructure/testhelper/platform_info.go @@ -0,0 +1,9 @@ +package testhelper + +func (i infoHandler) populatePlatformInfoHandler() error { + return nil +} + +func (i infoHandler) populatePortInfoHandler() error { + return nil +} diff --git a/infrastructure/testhelper/platform_info/BUILD.bazel b/infrastructure/testhelper/platform_info/BUILD.bazel new file mode 100644 index 0000000..c33a96e --- /dev/null +++ b/infrastructure/testhelper/platform_info/BUILD.bazel @@ -0,0 +1,10 @@ +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +filegroup( + name = "platform_info", + srcs = glob(["*.go"]), + visibility = ["//visibility:public"], +) diff --git a/infrastructure/testhelper/platform_info/default.go b/infrastructure/testhelper/platform_info/default.go new file mode 100644 index 0000000..8b6c09b --- /dev/null +++ b/infrastructure/testhelper/platform_info/default.go @@ -0,0 +1,48 @@ +package testhelper + +import ( + "testing" + + "github.com/openconfig/ondatra" +) + +type defaultPlatform struct { + infoBuilder + platformInfo PlatformInfo + portInfo PortInfo +} + +var platform = defaultPlatform{ + platformInfo: PlatformInfo{ + SystemInfo: SystemInfo{ + RebootTime: 360000000000, + }, + HardwareInfo: HardwareInfo{}, + }, + portInfo: PortInfo{ + MaxLanes: 8, + PMD: map[PMDType]bool{ + "ETH_200GBASE_BSM8": true, + "ETH_2X200GBASE_BGR4": true, + "ETH_2X400GBASE_CDGR4_PLUS": true, + "ETH_2X400GBASE_CR4": true, + "ETH_2X400GBASE_DR4": true, + "ETH_2X400GBASE_PSM4": true, + }, + PortProperties: map[string]*PortProperty{}, + }, +} + +func (d *defaultPlatform) newPlatformInfo(t *testing.T, dut *ondatra.DUTDevice) (*PlatformInfo, error) { + ret := d.platformInfo + return &ret, nil +} + +func (d *defaultPlatform) newPortInfo(t *testing.T, dut *ondatra.DUTDevice) (*PortInfo, error) { + ret := d.portInfo + return &ret, nil +} + +func init() { + registerPlatform("default", &platform) +} diff --git a/infrastructure/testhelper/platform_info/platform_info.go b/infrastructure/testhelper/platform_info/platform_info.go new file mode 100644 index 0000000..eb727a6 --- /dev/null +++ b/infrastructure/testhelper/platform_info/platform_info.go @@ -0,0 +1,294 @@ +package testhelper + +import ( + "fmt" + "log" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi/oc" +) + +// LoggingInfo contains a remote server addresses to be used for logging. +type LoggingInfo struct { + IPv4RemoteAddresses []string + IPv6RemoteAddresses []string +} + +// CPUInfo contains CPU-related information. +type CPUInfo struct { + Index uint32 + MaxAverageUsage uint8 +} + +// MemoryInfo contains memory related information. +type MemoryInfo struct { + Physical uint64 + FreeThreshold uint64 + UsedThreshold uint64 + CorrectableEccErrorThreshold uint64 +} + +// NTPServerInfo returns NTP server related information. +type NTPServerInfo struct { + IPv4Address []string + IPv6Address []string + StratumThreshold uint8 +} + +// FPGAInfo consists of FPGA related information. +type FPGAInfo struct { + Name string + Manufacturer string + Description string + FirmwareVersionRegex string + ResetCauseNum int +} + +// IntegratedCircuitInfo consists of integrated-circuit related information. +type IntegratedCircuitInfo struct { + Name string + CorrectedParityErrorsThreshold uint64 +} + +// Threshold32 consists of the minimum and maximum thresholds as a float32. +type Threshold32 struct { + Min float32 + Max float32 +} + +// Threshold64 consists of the minimum and maximum thresholds as a float64. +type Threshold64 struct { + Min float64 + Max float64 +} + +// TemperatureSensorInfo consists of temperature sensor related information. +type TemperatureSensorInfo struct { + Name string + Location string + MaxTemperature float64 +} + +// SecurityComponentInfo consists of security component related information. +type SecurityComponentInfo struct { + Name string +} + +// Threshold is any numeric type that is used as a lower or upper threshold. +type Threshold interface { + float64 | uint64 | uint32 +} + +// Thresholds encapsulates a set of inclusive lower and upper thresholds. +type Thresholds[T Threshold] struct { + HasLo bool + Lo T + HasHi bool + Hi T +} + +// SmartDataInfo consists of storage device SMART data related information. +type SmartDataInfo struct { + WriteAmplificationFactorThresholds Thresholds[float64] + RawReadErrorRateThresholds Thresholds[float64] + ThroughputPerformanceThresholds Thresholds[float64] + ReallocatedSectorCountThresholds Thresholds[uint64] + PowerOnSecondsThresholds Thresholds[uint64] + SSDLifeLeftThresholds Thresholds[uint64] + AvgEraseCountThresholds Thresholds[uint32] + MaxEraseCountThresholds Thresholds[uint32] +} + +// StorageDeviceInfo consists of storage device related information. +type StorageDeviceInfo struct { + Name string + IsRemovable bool + IOErrorsThreshold uint64 + SmartDataInfo SmartDataInfo +} + +// FanInfo consists of fan related information. +type FanInfo struct { + Name string + IsRemovable bool + Parent string + Location string + MaxSpeed uint32 +} + +// PcieInfo consists of PCIe device related information. +type PCIeInfo struct { + Name string +} + +// FanTrayInfo consists of fan tray related information. +type FanTrayInfo struct { + Name string + IsRemovable bool + Parent string + Location string +} + +// MountPointInfo returns mount points related information. +type MountPointInfo struct { + Name string +} + +// HardwareInfo contains hardware components related information. +type HardwareInfo struct { + Fans []FanInfo + Fantrays []FanTrayInfo + FPGAs []FPGAInfo + ICs []IntegratedCircuitInfo + PCIe []PCIeInfo + Security []SecurityComponentInfo + Storage []StorageDeviceInfo + CPU []TemperatureSensorInfo + Heatsink []TemperatureSensorInfo + Exhaust []TemperatureSensorInfo + Inlet []TemperatureSensorInfo + Dimm []TemperatureSensorInfo +} + +// SystemInfo consists of system related information. +type SystemInfo struct { + RebootTime time.Duration + CPUInfo []CPUInfo + LoggingInfo LoggingInfo + MemInfo MemoryInfo + MountPointInfo []MountPointInfo + NTPServerInfo []NTPServerInfo +} + +// PlatformInfo contains platform specific information. +type PlatformInfo struct { + SystemInfo SystemInfo + HardwareInfo HardwareInfo + build func(t *testing.T, dut *ondatra.DUTDevice, p *PlatformInfo) error +} + +// Lanes represents number of lanes. +type Lanes int + +// PMDProperty contain PMD information. +type PMDProperty struct { + SupportedSpeeds map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED + SupportedBreakoutModes []string + CollateralFlap bool +} + +type PMDType string + +var pmdProperties = map[PMDType]*PMDProperty{ + "ETH_2X400GBASE_PSM4": &PMDProperty{ + CollateralFlap: false, + SupportedSpeeds: map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED{ + 4: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB}, + 2: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB}, + }, + SupportedBreakoutModes: []string{"2x400G", "4x200G", "1x400G(4)+2x200G(4)", "2x200G(4),+1x400G(4)"}, + }, + "ETH_2X400GBASE_DR4": &PMDProperty{ + CollateralFlap: false, + SupportedSpeeds: map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED{ + 4: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB}, + 2: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB}, + 1: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_100GB}, + }, + SupportedBreakoutModes: []string{"8x100G", "2x400G", "4x200G", "1x400G(4)+2x200G(4)", "2x200G(4)+1x400G(4)"}, + }, + "ETH_2X200GBASE_BGR4": &PMDProperty{ + CollateralFlap: false, + SupportedSpeeds: map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED{ + 4: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB}, + 2: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_50GB}, + }, + SupportedBreakoutModes: []string{"1x200G(4)+2x50G(4)", "2x50G(4)+1x200G(4)"}, + }, + + "ETH_200GBASE_BSM8": &PMDProperty{ + CollateralFlap: false, + SupportedSpeeds: map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED{ + 2: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_50GB}, + }, + SupportedBreakoutModes: []string{"1x200G(4)+2x50G(4)", "2x50G(4)+1x200G(4)"}, + }, + "ETH_2X400GBASE_CDGR4_PLUS": &PMDProperty{ + CollateralFlap: true, + SupportedSpeeds: map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED{ + 4: []oc.E_IfEthernet_ETHERNET_SPEED{ + oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB, + oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB, + oc.IfEthernet_ETHERNET_SPEED_SPEED_100GB, + }, + }, + SupportedBreakoutModes: []string{"2x400G", "2x200G", "2x100G", "1x400G(4)+1x200G(4)", "1x400G(4)+1x100G(4)", "1x200G(4)+1x400G(4)", "1x200G(4)+1x100G(4)", "1x100G(4)+1x400G(4)", "1x100G(4)+1x200G(4)"}, + }, + "ETH_2X400GBASE_CR4": &PMDProperty{ + CollateralFlap: false, + SupportedSpeeds: map[Lanes][]oc.E_IfEthernet_ETHERNET_SPEED{ + 4: []oc.E_IfEthernet_ETHERNET_SPEED{oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB}, + }, + SupportedBreakoutModes: []string{"2x400G"}, + }, +} + +// PortProperties contains front panel port information. +type PortProperty struct { + Index int + DefaultBreakoutMode string + MediaType string +} + +func (p *PortInfo) PMDProperty(pmdType PMDType) (*PMDProperty, error) { + if _, ok := p.PMD[pmdType]; !ok { + return nil, fmt.Errorf("PMDType : %v not supported by the PortInfo", pmdType) + } + if v, ok := pmdProperties[pmdType]; ok { + ret := *v + return &ret, nil + } + return nil, fmt.Errorf("PMDType : %v not defined in pmdProperties", pmdType) +} + +// PortInfo contains port related information. +type PortInfo struct { + MaxLanes int + PortProperties map[string]*PortProperty + PMD map[PMDType]bool + build func(t *testing.T, dut *ondatra.DUTDevice, p *PortInfo) error +} + +type infoBuilder interface { + newPlatformInfo(t *testing.T, dut *ondatra.DUTDevice) (*PlatformInfo, error) + newPortInfo(t *testing.T, dut *ondatra.DUTDevice) (*PortInfo, error) +} + +var platforms = map[string]infoBuilder{} + +func registerPlatform(platformName string, val infoBuilder) { + if _, ok := platforms[platformName]; ok { + log.Fatalf("platform : %v already registered.", platformName) + } + platforms[platformName] = val +} + +// NewPortInfo creates a new PortInfo. +func NewPortInfo(t *testing.T, dut *ondatra.DUTDevice, platformName string) (*PortInfo, error) { + val, ok := platforms[platformName] + if !ok { + return nil, fmt.Errorf("PortInfo struct not found for : %v", platformName) + } + return val.newPortInfo(t, dut) +} + +// NewPlatformInfo creates a new PlatformInfo. +func NewPlatformInfo(t *testing.T, dut *ondatra.DUTDevice, platformName string) (*PlatformInfo, error) { + val, ok := platforms[platformName] + if !ok { + return nil, fmt.Errorf("PlatformInfo struct not found for : %v", platformName) + } + return val.newPlatformInfo(t, dut) +} diff --git a/infrastructure/testhelper/port_management.go b/infrastructure/testhelper/port_management.go new file mode 100644 index 0000000..8c8a514 --- /dev/null +++ b/infrastructure/testhelper/port_management.go @@ -0,0 +1,972 @@ +package testhelper + +// This file provides helper APIs to perform ports related operations. + +import ( + "fmt" + "strconv" + "strings" + "testing" + "time" + "math" + + log "github.com/golang/glog" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/pkg/errors" +) + +type speedEnumInfo struct { + // Speed value in string format in bits/second. + speedStr string + // Speed value in integer format in bits/second. + speedInt uint64 +} + +var stringToEnumSpeedMap = map[string]oc.E_IfEthernet_ETHERNET_SPEED{ + "10M": oc.IfEthernet_ETHERNET_SPEED_SPEED_10MB, + "100M": oc.IfEthernet_ETHERNET_SPEED_SPEED_100MB, + "1G": oc.IfEthernet_ETHERNET_SPEED_SPEED_1GB, + "2500M": oc.IfEthernet_ETHERNET_SPEED_SPEED_2500MB, + "5G": oc.IfEthernet_ETHERNET_SPEED_SPEED_5GB, + "10G": oc.IfEthernet_ETHERNET_SPEED_SPEED_10GB, + "25G": oc.IfEthernet_ETHERNET_SPEED_SPEED_25GB, + "40G": oc.IfEthernet_ETHERNET_SPEED_SPEED_40GB, + "50G": oc.IfEthernet_ETHERNET_SPEED_SPEED_50GB, + "100G": oc.IfEthernet_ETHERNET_SPEED_SPEED_100GB, + "200G": oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB, + "400G": oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB, + "600G": oc.IfEthernet_ETHERNET_SPEED_SPEED_600GB, + "800G": oc.IfEthernet_ETHERNET_SPEED_SPEED_800GB, +} + +var enumToSpeedInfoMap = map[oc.E_IfEthernet_ETHERNET_SPEED]speedEnumInfo{ + oc.IfEthernet_ETHERNET_SPEED_SPEED_10MB: {"10M", 10_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_100MB: {"100M", 100_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_1GB: {"1G", 1_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_2500MB: {"2500M", 2500_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_5GB: {"5G", 5_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_10GB: {"10G", 10_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_25GB: {"25G", 25_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_40GB: {"40G", 40_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_50GB: {"50G", 50_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_100GB: {"100G", 100_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB: {"200G", 200_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB: {"400G", 400_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_600GB: {"600G", 600_000_000_000}, + oc.IfEthernet_ETHERNET_SPEED_SPEED_800GB: {"800G", 800_000_000_000}, +} + +// Indices for slot, port and lane number in Ethernet port naming format. +const ( + slotIndex int = iota + portIndex + laneIndex +) + +// PortProperties contains front panel port information. +type PortProperties struct { + index int + supportedSpeeds map[string]map[int][]oc.E_IfEthernet_ETHERNET_SPEED + defaultBreakoutMode string + supportedBreakoutModes map[string][]string + mediaType string +} + +// RandomPortBreakoutInfo contains information about a randomly picked port on the switch. +type RandomPortBreakoutInfo struct { + PortName string // Randomly selected port on switch. + CurrBreakoutMode string // Currently configured breakout mode on the port. + SupportedBreakoutMode string // Supported breakout mode on port different from current breakout mode. +} + +// BreakoutType describes the possible types of breakout modes +type BreakoutType int + +const ( + // Unset indicates a not set breakout mode to be used where breakout is not applicable. + Unset BreakoutType = iota + // Any indicates any breakout modes (mixed as well as non-mixed) + Any + // Mixed indicates mixed breakout modes only + Mixed + // NonMixed indicates non mixed breakout only + NonMixed + // Channelized indicates breakout mode with at least one more port other than parent port. + // This mode is used to test breakout with subinterface config on child port. + Channelized + // SpeedChangeOnly indicates breakout mode that results in a speed change only (no lane change) on requested number of ports. + SpeedChangeOnly +) + +// PortBreakoutInfo contains list of resultant ports for a given breakout mode and physical channels and operational status for each interface. +type PortBreakoutInfo struct { + PhysicalChannels []uint16 + OperStatus oc.E_Interface_OperStatus + PortSpeed oc.E_IfEthernet_ETHERNET_SPEED +} + +// RandomPortWithSupportedBreakoutModesParams contains list of additional parameters for RandomPortWithSupportedBreakoutModes +type RandomPortWithSupportedBreakoutModesParams struct { + CurrBreakoutType BreakoutType // mixed/non-mixed/any/channelized + NewBreakoutType BreakoutType // mixed/non-mixed/any/channelized + SpeedChangeOnlyPortCount int // number of ports that are required to change in speed only on breakout + PortList []string // List of ports from which a random port can be selected +} + +// Uint16ListToString returns comma separate string representation of list of uint16. +func Uint16ListToString(a []uint16) string { + s := make([]string, len(a)) + for index, value := range a { + s[index] = strconv.Itoa(int(value)) + } + return strings.Join(s, ",") +} + +// CollateralFlapAllowed indicates if collateral link flap is allowed on the platform, pmd type. +func CollateralFlapAllowed(t *testing.T, dut *ondatra.DUTDevice, pmdType string) (bool, error) { + info, err := portInfoForDevice(t, dut) + if err != nil { + return false, errors.Wrap(err, "failed to fetch platform specific information") + } + if pmdProperty, err := info.PMDProperty(PMDType(pmdType)); err == nil { + return pmdProperty.CollateralFlap, nil + } + + // Assume collateral flap is not allowed if entry doesn't exist! + log.Infof("Update collateralFlap map to include PMD type: %v!", pmdType) + return false, nil +} + +// EthernetSpeedToBpsString returns speed in string format in bits/second. +func EthernetSpeedToBpsString(speed oc.E_IfEthernet_ETHERNET_SPEED) (string, error) { + if _, ok := enumToSpeedInfoMap[speed]; !ok { + return "", errors.Errorf("invalid speed (%v) found", speed) + } + return enumToSpeedInfoMap[speed].speedStr, nil +} + +// EthernetSpeedToUint64 returns the speed in uint64 format. +func EthernetSpeedToUint64(speed oc.E_IfEthernet_ETHERNET_SPEED) (uint64, error) { + if _, ok := enumToSpeedInfoMap[speed]; !ok { + return 0, errors.Errorf("invalid speed (%v) found", speed) + } + return enumToSpeedInfoMap[speed].speedInt, nil +} + +// FrontPanelPortToIndexMappingForDevice returns list of front panel port to index mapping. +func FrontPanelPortToIndexMappingForDevice(t *testing.T, dut *ondatra.DUTDevice) (map[string]int, error) { + info, err := portInfoForDevice(t, dut) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch platform specific information") + } + portToIndexMap := make(map[string]int) + for port, value := range info.PortProperties { + portToIndexMap[port] = value.Index + } + return portToIndexMap, nil +} + +// SupportedSpeedsForPort returns list of supported speeds for given interface. +func SupportedSpeedsForPort(t *testing.T, dut *ondatra.DUTDevice, interfaceName string) ([]oc.E_IfEthernet_ETHERNET_SPEED, error) { + info, err := portInfoForDevice(t, dut) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch platform specific information") + } + lanes := len(testhelperIntfPhysicalChannelsGet(t, dut, interfaceName)) + pmd, err := testhelperPortPmdTypeGet(t, dut, interfaceName) + if err != nil { + return nil, err + } + pmdProperty, err := info.PMDProperty(PMDType(pmd)) + if err != nil { + return nil, err + } + if v := pmdProperty.SupportedSpeeds[Lanes(lanes)]; len(v) != 0 { + return v, nil + } + return nil, errors.Errorf("no supported speeds found for interface %v pmd %v with %v lanes", interfaceName, pmd, lanes) +} + +// TransceiverEmpty returns true if the transceiver status is 0, false if the status is 1 +func TransceiverEmpty(t *testing.T, d *ondatra.DUTDevice, port string) (bool, error) { + transceiverNumber, err := TransceiverNumberForPort(t, d, port) + if err != nil { + return false, err + } + return testhelperTransceiverEmpty(t, d, FrontPanelPortPrefix+strconv.Itoa(transceiverNumber)), nil +} + +// MaxLanesPerPort returns the maximum number of ASIC lanes per port on the dut. +func MaxLanesPerPort(t *testing.T, dut *ondatra.DUTDevice) (uint8, error) { + info, err := portInfoForDevice(t, dut) + if err != nil { + return 0, errors.Wrap(err, "failed to fetch platform specific information") + } + return uint8(info.MaxLanes), nil +} + +func breakoutModeFromGroup(port string, groups *oc.Component_Port_BreakoutMode) (string, error) { + currentBreakoutMode := "" + // Use 0 based index to access breakout groups in increasing index order. + index := uint8(0) + _, ok := groups.Group[index] + for ok == true { + if index > 0 { + currentBreakoutMode += "+" + } + breakoutSpeed := groups.Group[index].GetBreakoutSpeed() + breakoutSpeedStr, err := EthernetSpeedToBpsString(breakoutSpeed) + if err != nil { + return "", err + } + currentBreakoutMode += strconv.Itoa(int(groups.Group[index].GetNumBreakouts())) + "x" + breakoutSpeedStr + index++ + _, ok = groups.Group[index] + } + return currentBreakoutMode, nil +} + +// CurrentBreakoutModeForPort returns the currently configured breakout mode for given port. +func CurrentBreakoutModeForPort(t *testing.T, dut *ondatra.DUTDevice, port string) (string, error) { + // Check if requested port is a parent port. Breakout is applicable to parent port only. + isParent, err := IsParentPort(t, dut, port) + if err != nil { + return "", errors.Wrap(err, "IsParentPort() failed") + } + if !isParent { + return "", errors.Errorf("port: %v is not a parent port", port) + } + // Get the physical port for given port. + physicalPort, err := PhysicalPort(t, dut, port) + if err != nil { + return "", errors.Errorf("failed to get physical port for interface %v", port) + } + // Get breakout group information from component state paths. + groups := testhelperBreakoutModeGet(t, dut, physicalPort) + if groups == nil { + return "", errors.Errorf("failed to get breakout mode for port %v", port) + } + return breakoutModeFromGroup(port, groups) +} + +// SupportedBreakoutModesForPort returns list of supported breakout modes for given interface. +func SupportedBreakoutModesForPort(t *testing.T, dut *ondatra.DUTDevice, interfaceName string, breakoutType BreakoutType) ([]string, error) { + info, err := portInfoForDevice(t, dut) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch platform specific information") + } + _, ok := info.PortProperties[interfaceName] + if !ok { + return nil, errors.Errorf("no entry found for interface %v in front panel port list", interfaceName) + } + + pmd, err := testhelperPortPmdTypeGet(t, dut, interfaceName) + if err != nil { + return nil, err + } + + // TODO: the function should take port into consideration. + pmdProperty, err := info.PMDProperty(PMDType(pmd)) + if err != nil { + return nil, err + } + + // Return requested type of breakout modes only. + var supportedBreakoutModesOfBreakoutType []string + if breakoutType == Mixed { + for _, mode := range pmdProperty.SupportedBreakoutModes { + if strings.Contains(mode, "+") { + supportedBreakoutModesOfBreakoutType = append(supportedBreakoutModesOfBreakoutType, mode) + } + } + //pmdProperty.SupportedBreakoutModes = supportedBreakoutModesOfBreakoutType + } + if breakoutType == NonMixed { + for _, mode := range pmdProperty.SupportedBreakoutModes { + if !strings.Contains(mode, "+") { + supportedBreakoutModesOfBreakoutType = append(supportedBreakoutModesOfBreakoutType, mode) + } + } + //pmdProperty.SupportedBreakoutModes = supportedBreakoutModesOfBreakoutType + } + if breakoutType == Channelized { + for _, mode := range pmdProperty.SupportedBreakoutModes { + values := strings.Split(mode, "x") + if len(values) < 2 { + return nil, errors.Errorf("invalid breakout format (%v)", mode) + } + numBreakouts, err := strconv.Atoi(values[0]) + if err != nil { + return nil, errors.Wrapf(err, "error parsing numBreakouts for breakout mode %v", mode) + } + if strings.Contains(mode, "+") || numBreakouts > 1 { + supportedBreakoutModesOfBreakoutType = append(supportedBreakoutModesOfBreakoutType, mode) + } + } + //pmdProperty.SupportedBreakoutModes = supportedBreakoutModesOfBreakoutType + } + return supportedBreakoutModesOfBreakoutType, nil +} + +// PortMediaType returns the media type of the requested port. +func PortMediaType(t *testing.T, dut *ondatra.DUTDevice, interfaceName string) (string, error) { + info, err := portInfoForDevice(t, dut) + if err != nil { + return "", errors.Wrap(err, "failed to fetch platform specific information") + } + port, ok := info.PortProperties[interfaceName] + if !ok { + return "", errors.Errorf("no entry found for interface %v in front panel port list", interfaceName) + } + return port.MediaType, nil +} + +func slotPortLaneForPort(port string) ([]string, error) { + if !IsFrontPanelPort(port) { + return nil, errors.Errorf("requested port (%v) is not a front panel port", port) + } + slotPortLane := port[len(FrontPanelPortPrefix):] + values := strings.Split(slotPortLane, "/") + if len(values) != 3 { + return nil, errors.Errorf("invalid port name format for port %v", port) + } + return values, nil +} + +// ExpectedPortInfoForBreakoutMode returns the expected port list, physical channels and port speed for a given breakout mode. +// Eg. Ethernet0 configured to a breakout mode of "2x100G(4) + 1x200G(4)" will return the following: +// Ethernet0:{0,1}, Ethernet2:{2,3}, Ethernet4:{4,5,6,7} +// The number of physical channels per breakout mode is used to compute the offset from the parent port number. +func ExpectedPortInfoForBreakoutMode(t *testing.T, dut *ondatra.DUTDevice, interfaceName string, breakoutMode string) (map[string]*PortBreakoutInfo, error) { + if len(breakoutMode) == 0 { + return nil, errors.Errorf("found empty breakout mode") + } + // For a mixed breakout mode, get "+" separated breakout groups. + // Eg. For a mixed breakout mode of "2x100G(4) + 1x200G(4)"; modes = {2x100G(4), 1x200G(4)} + modes := strings.Split(breakoutMode, "+") + // Get maximum physical channels in a breakout group which is max lanes per physical port/number of groups in a breakout mode. + maxLanes, err := MaxLanesPerPort(t, dut) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch max lanes") + } + maxChannelsInGroup := int(maxLanes) / len(modes) + slotPortLane, err := slotPortLaneForPort(interfaceName) + if err != nil { + return nil, err + } + currLaneNumber, err := strconv.Atoi(slotPortLane[laneIndex]) + if err != nil { + return nil, errors.Wrapf(err, "failed to convert lane number (%v) to int", currLaneNumber) + } + + // For each breakout group, get numBreakouts and breakoutSpeed. Breakout group is in the format "numBreakouts x breakoutSpeed" + // Eg. mode = 2x100G + currPhysicalChannel := 0 + portBreakoutInfo := make(map[string]*PortBreakoutInfo) + interfaceToPhysicalChannelsMap := make(map[string][]uint16) + for _, mode := range modes { + values := strings.Split(mode, "x") + if len(values) != 2 { + return nil, errors.Errorf("invalid breakout format (%v)", mode) + } + numBreakouts, err := strconv.Atoi(values[0]) + if err != nil { + return nil, errors.Wrapf(err, "error parsing numBreakouts for breakout mode %v", mode) + } + // Extract speed from breakout_speed(num_physical_channels) eg:100G(4) + speed := strings.Split(values[1], "(") + breakoutSpeed, ok := stringToEnumSpeedMap[speed[0]] + if !ok { + return nil, errors.Errorf("found invalid breakout speed (%v) when parsing breakout mode %v", values[1], mode) + } + // For each resulting interface, construct the front panel interface name using offset from the parent port. + // For a breakout mode of Ethernet0 => 2x100G(4)+1x200G(4), the max channels per group would be 4 (considering 8 max lanes per physical port). + // Hence, breakout mode 2x100G (numBreakouts=2) would have an offset of 2 and 1x200G(numBreakouts=1) would have an offset of 1 + // leading to interfaces Ethernet0, Ethernet2 for mode 2x100G and Ethernet4 for mode 1x200G. + for i := 0; i < numBreakouts; i++ { + port := fmt.Sprintf("%s%s/%s/%d", FrontPanelPortPrefix, slotPortLane[slotIndex], slotPortLane[portIndex], currLaneNumber) + // Populate expected physical channels for each port. + // Physical channels are between 0 to 7. + offset := maxChannelsInGroup / numBreakouts + for j := currPhysicalChannel; j < offset+currPhysicalChannel; j++ { + interfaceToPhysicalChannelsMap[port] = append(interfaceToPhysicalChannelsMap[port], uint16(j)) + } + currPhysicalChannel += offset + currLaneNumber += offset + portBreakoutInfo[port] = &PortBreakoutInfo{ + PhysicalChannels: interfaceToPhysicalChannelsMap[port], + PortSpeed: breakoutSpeed, + } + } + } + return portBreakoutInfo, nil +} + +func computePortIDForPort(t *testing.T, d *ondatra.DUTDevice, intfName string) (uint32, error) { + // Try to get currently configured id for the port from the switch. + var id int + var laneindexuint32 uint32 + var Id uint32 + id, err := testhelperPortIDGet(t, d, intfName) + // Generate ID same as that used by controller, if not found on switch. + if err != nil { + isParent, err := IsParentPort(t, d, intfName) + if err != nil { + return 0, err + } + parentPortNumberStr, err := ParentPortNumber(intfName) + if err != nil { + return 0, err + } + parentPortNumber, err := conversionTouint32(parentPortNumberStr) + if err != nil { + return 0, err + } + // Port ID is same as port index/parent port number for parent ports. + if isParent { + return parentPortNumber, nil + } + // Port ID is computed for child ports using + // (laneIndex*512 + parentPortNumber + 1) + slotPortLane, err := slotPortLaneForPort(intfName) + if err != nil { + return 0, err + } + laneIndex, err := strconv.Atoi(slotPortLane[laneIndex]) + if err != nil { + return 0, err + } + laneindexuint32 = uint32(laneIndex*512) + return (laneindexuint32 + parentPortNumber + 1), nil + } + Id = uint32(id) + return Id, nil +} + +func fecMode(portSpeed oc.E_IfEthernet_ETHERNET_SPEED, lanes uint8) oc.E_IfEthernet_INTERFACE_FEC { + switch portSpeed { + case oc.IfEthernet_ETHERNET_SPEED_SPEED_400GB: + return oc.IfEthernet_INTERFACE_FEC_FEC_RS544_2X_INTERLEAVE + case oc.IfEthernet_ETHERNET_SPEED_SPEED_200GB: + return oc.IfEthernet_INTERFACE_FEC_FEC_RS544_2X_INTERLEAVE + case oc.IfEthernet_ETHERNET_SPEED_SPEED_100GB: + switch lanes { + case 1, 2: + return oc.IfEthernet_INTERFACE_FEC_FEC_RS544 + case 4: + return oc.IfEthernet_INTERFACE_FEC_FEC_RS528 + } + case oc.IfEthernet_ETHERNET_SPEED_SPEED_50GB: + switch lanes { + case 1: + return oc.IfEthernet_INTERFACE_FEC_FEC_RS544 + case 2: + return oc.IfEthernet_INTERFACE_FEC_FEC_DISABLED + } + } + + return oc.IfEthernet_INTERFACE_FEC_FEC_DISABLED +} + +func interfaceConfigForPort(t *testing.T, d *ondatra.DUTDevice, intfName string, breakoutSpeed oc.E_IfEthernet_ETHERNET_SPEED, fec oc.E_IfEthernet_INTERFACE_FEC) (*oc.Interface, error) { + subinterfaceIndex := uint32(0) + unnumberedEnabled := true + mtu := uint16(9216) + enabled := true + id, err := computePortIDForPort(t, d, intfName) + if err != nil { + return nil, err + } + interfaceConfig := &oc.Interface{ + Enabled: &enabled, + LoopbackMode: oc.Interfaces_LoopbackModeType_NONE, + Mtu: &mtu, + Name: &intfName, + Id: &id, + Ethernet: &oc.Interface_Ethernet{ + PortSpeed: breakoutSpeed, + FecMode: fec, + }, + Subinterface: map[uint32]*oc.Interface_Subinterface{ + 0: { + Index: &subinterfaceIndex, + Ipv6: &oc.Interface_Subinterface_Ipv6{ + Unnumbered: &oc.Interface_Subinterface_Ipv6_Unnumbered{ + Enabled: &unnumberedEnabled, + }, + }, + }, + }, + } + return interfaceConfig, nil +} + +// ConfigFromBreakoutMode returns config with component and interface paths for given breakout mode. +// Breakout mode is in the format "numBreakouts1 x breakoutSpeed1 + numBreakouts2 x breakoutSpeed2 + ... +// Eg: "1x400G", 2x100G(4) + 1x200G(4)" +func ConfigFromBreakoutMode(t *testing.T, dut *ondatra.DUTDevice, breakoutMode, port string) (*oc.Root, error) { + if len(breakoutMode) == 0 { + return nil, errors.Errorf("found empty breakout mode") + } + + // Check if requested port is a parent port. Breakout is applicable to parent port only. + isParent, err := IsParentPort(t, dut, port) + if err != nil { + return nil, errors.Wrap(err, "IsParentPort() failed") + } + if !isParent { + return nil, errors.Errorf("port: %v is not a parent port", port) + } + // Get lane number for port. + slotPortLane, err := slotPortLaneForPort(port) + if err != nil { + return nil, err + } + currLaneNumber, err := strconv.Atoi(slotPortLane[laneIndex]) + if err != nil { + return nil, errors.Wrapf(err, "failed to convert lane number (%v) to int", currLaneNumber) + } + + maxLanes, err := MaxLanesPerPort(t, dut) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch max lanes") + } + + // For a mixed breakout mode, get "+" separated breakout groups. + // Eg. For a breakout mode of "2x100G(4)+1x200G(4)", modes = {2x100G, 1x200G} + modes := strings.Split(breakoutMode, "+") + // Get maximum physical channels in a breakout group which is max lanes per physical port/number of groups in a breakout mode. + maxChannelsInGroup := maxLanes / uint8(len(modes)) + index := 0 + breakoutGroups := make(map[uint8]*oc.Component_Port_BreakoutMode_Group) + interfaceConfig := make(map[string]*oc.Interface) + + // For each breakout group, get numBreakouts and breakoutSpeed. Breakout group is in the format "numBreakouts x breakoutSpeed(numPhysicalChannels)" + // Eg. 2x100G(4) + for _, mode := range modes { + values := strings.Split(mode, "x") + if len(values) != 2 { + return nil, errors.Errorf("invalid breakout format (%v)", mode) + } + numBreakouts, err := portStringToUint8(values[0]) + if err != nil { + return nil, errors.Wrapf(err, "error parsing numBreakouts for breakout mode %v", mode) + } + u8numBreakouts := numBreakouts + // Extract speed from breakout_speed(num_physical_channels) eg:100G(4) + speed := strings.Split(values[1], "(") + breakoutSpeed, ok := stringToEnumSpeedMap[speed[0]] + if !ok { + return nil, errors.Errorf("found invalid breakout speed (%v) when parsing breakout mode %v", values[1], mode) + } + // Physical channels per breakout group are equally divided amongst breakouts in the group. + numPhysicalChannels := maxChannelsInGroup / numBreakouts + currIndex := uint8(index) + // Construct config corresponding to each breakout group. + group := oc.Component_Port_BreakoutMode_Group{ + Index: &currIndex, + BreakoutSpeed: breakoutSpeed, + NumBreakouts: &u8numBreakouts, + NumPhysicalChannels: &numPhysicalChannels, + } + // Add breakout group config to breakout config using index as key. + // Index is strictly ordered staring from 0. + breakoutGroups[currIndex] = &group + + // Get the interface config for all interfaces corresponding to current breakout group. + for i := 1; i <= int(numBreakouts); i++ { + intfName := fmt.Sprintf("%s%s/%s/%d", FrontPanelPortPrefix, slotPortLane[slotIndex], slotPortLane[portIndex], currLaneNumber) + interfaceConfig[intfName], err = interfaceConfigForPort(t, dut, intfName, breakoutSpeed, fecMode(breakoutSpeed, numPhysicalChannels)) + if err != nil { + return nil, err + } + offset := int(maxChannelsInGroup) / int(numBreakouts) + currLaneNumber += offset + } + index++ + } + + // Get port ID. + frontPanelPortToIndexMap, err := FrontPanelPortToIndexMappingForDevice(t, dut) + if err != nil { + return nil, errors.Errorf("failed to fetch front panel port to index mapping from device %v", testhelperDUTNameGet(dut)) + } + if _, ok := frontPanelPortToIndexMap[port]; !ok { + return nil, errors.Errorf("port %v not found in list of front panel port", port) + } + portIndex := frontPanelPortToIndexMap[port] + + // Construct component path config from created breakout groups. + componentName := "1/" + strconv.Itoa(portIndex) + componentConfig := map[string]*oc.Component{ + componentName: { + Name: &componentName, + Port: &oc.Component_Port{ + BreakoutMode: &oc.Component_Port_BreakoutMode{Group: breakoutGroups}, + }, + }, + } + + // Construct overall config from component and interface config. + deviceConfig := &oc.Root{ + Interface: interfaceConfig, + Component: componentConfig, + } + return deviceConfig, nil +} + +// SpeedChangeOnlyPorts returns +// 1. Whether changing from currBrekaoutMode to newBreakoutMode is overall a speed change operation. +// 2. Number of ports that will result in speed change only if 1 is true. +func SpeedChangeOnlyPorts(t *testing.T, dut *ondatra.DUTDevice, port string, currBreakoutMode string, newBreakoutMode string) (bool, int, error) { + t.Helper() + // Get list of interfaces for current and new breakout modes. + currPortInfo, err := ExpectedPortInfoForBreakoutMode(t, dut, port, currBreakoutMode) + if err != nil { + return false, 0, errors.Wrapf(err, "failed to get expected port information for breakout mode (%v) for port %v", currBreakoutMode, port) + } + if currPortInfo == nil { + return false, 0, errors.Errorf("got empty port information for breakout mode %v for port %v", currBreakoutMode, port) + } + newPortInfo, err := ExpectedPortInfoForBreakoutMode(t, dut, port, newBreakoutMode) + if err != nil { + return false, 0, errors.Wrapf(err, "failed to get expected port information for breakout mode (%v) for port %v", newBreakoutMode, port) + } + if newPortInfo == nil { + return false, 0, errors.Errorf("got empty port information for breakout mode %v for port %v", newBreakoutMode, port) + } + speedChangeOnlyPortCount := 0 + unchangedPortCount := 0 + for port, info := range currPortInfo { + if _, ok := newPortInfo[port]; ok { + if Uint16ListToString(info.PhysicalChannels) == Uint16ListToString(newPortInfo[port].PhysicalChannels) { + if info.PortSpeed != newPortInfo[port].PortSpeed { + speedChangeOnlyPortCount++ + } else { + unchangedPortCount++ + } + } + } else { + return false, 0, nil + } + } + return ((speedChangeOnlyPortCount + unchangedPortCount) == len(currPortInfo)), speedChangeOnlyPortCount, nil +} + +func breakoutModeSupportedTypes(breakoutMode string) (map[BreakoutType]bool, error) { + supportedBreakoutTypes := map[BreakoutType]bool{ + Any: true, + } + if strings.Contains(breakoutMode, "+") { + supportedBreakoutTypes[Mixed] = true + supportedBreakoutTypes[Channelized] = true + } else { + supportedBreakoutTypes[NonMixed] = true + values := strings.Split(breakoutMode, "x") + if len(values) != 2 { + return nil, errors.Errorf("invalid breakout format (%v)", breakoutMode) + } + numBreakouts, err := strconv.Atoi(values[0]) + if err != nil { + return nil, errors.Wrapf(err, "error parsing numBreakouts for breakout mode %v", breakoutMode) + } + if numBreakouts > 1 { + supportedBreakoutTypes[Channelized] = true + } + } + return supportedBreakoutTypes, nil +} + +// RandomPortWithSupportedBreakoutModes attempts to get a random port from list of front panel ports +// that supports at least one more breakout mode other than the currently configured breakout mode. +func RandomPortWithSupportedBreakoutModes(t *testing.T, dut *ondatra.DUTDevice, params *RandomPortWithSupportedBreakoutModesParams) (*RandomPortBreakoutInfo, error) { + t.Helper() + var portList []string + newBreakoutType := Unset + currBreakoutType := Unset + reqSpeedChangeOnlyPortCount := 0 + // Parse additional parameters + if params != nil { + portList = params.PortList + newBreakoutType = params.NewBreakoutType + currBreakoutType = params.CurrBreakoutType + reqSpeedChangeOnlyPortCount = params.SpeedChangeOnlyPortCount + } + // A port is randomly picked from given list (we start with all front panel ports if portList is not specified). + var err error + if len(portList) == 0 { + portList, err = FrontPanelPortListForDevice(t, dut) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch front panel port list") + } + } + + // Maintain a map of interfaces to allow fast deletion of port from portList (if it does not meet the test requirements). + portMap := make(map[string]bool) + for _, port := range portList { + portMap[port] = true + } + + // Keep trying to get a random port till one with at least one supported breakout mode is found. + var port, breakoutMode, currBreakoutMode string + for len(portMap) != 0 { + // Construct portList from port map. + var portList []string + for p := range portMap { + portList = append(portList, p) + } + randomInterfaceParams := RandomInterfaceParams{ + PortList: portList, + IsParent: true, + } + port, err = RandomInterface(t, dut, &randomInterfaceParams) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch random interface") + } + + // Get current breakout mode for the port. + currBreakoutMode, err = CurrentBreakoutModeForPort(t, dut, port) + if err != nil || currBreakoutMode == "" { + return nil, errors.Wrapf(err, "failed to fetch current breakout mode for port %v", port) + } + + // Supported breakout modes are not required for cases where only the current breakout mode for + // a port is of importance. + // Eg: Port sfec tests require a channelized port but do not perform any breakout operations, + // so the port is not required to support other breakout modes. + if newBreakoutType == Unset { + return &RandomPortBreakoutInfo{ + PortName: port, + CurrBreakoutMode: currBreakoutMode, + SupportedBreakoutMode: "", + }, + nil + } + + // Check if current breakout mode is of the requested type. + if currBreakoutType != Any { + currBreakoutTypes, err := breakoutModeSupportedTypes(currBreakoutMode) + if err != nil { + return nil, errors.Errorf("failed to get types supported by current breakout mode %v", currBreakoutMode) + } + + // Do not consider port if requested breakout mode type is not supported by the current breakout mode. + if _, ok := currBreakoutTypes[currBreakoutType]; !ok { + delete(portMap, port) + continue + } + } + + // Get supported breakout modes for the port. + supportedBreakoutModes, err := SupportedBreakoutModesForPort(t, dut, port, newBreakoutType) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch supported breakout modes for port %v", port) + } + if len(supportedBreakoutModes) == 0 { + if newBreakoutType == Mixed { + log.Infof("No supported mixed breakout modes found for port %v!", port) + delete(portMap, port) + continue + } else { + // Each port must support at least one breakout mode. + return nil, errors.Errorf("no supported breakout modes found for port %v", port) + } + } + + // Get a supported breakout mode different from current breakout mode. + // Ignore breakout modes that will only result in a speed change. + for _, mode := range supportedBreakoutModes { + speedChangeOnly, speedChangeOnlyPortCount, err := SpeedChangeOnlyPorts(t, dut, port, currBreakoutMode, mode) + if err != nil { + return nil, errors.Errorf("failed to determine if mode %v is a port speed change only from mode %v for port %v: %v", mode, currBreakoutMode, port, err) + } + if mode != currBreakoutMode { + if newBreakoutType != SpeedChangeOnly { + if !speedChangeOnly { + breakoutMode = mode + break + } + } else { + if speedChangeOnly && speedChangeOnlyPortCount >= reqSpeedChangeOnlyPortCount { + breakoutMode = mode + break + } + } + } + } + if breakoutMode != "" { + // Found a supported breakout mode other than current breakout mode. + break + } + + log.Infof("No other supported breakout mode found for port %v", port) + delete(portMap, port) + } + if breakoutMode == "" { + return nil, errors.Errorf("no ports with supported breakout modes found") + } + + log.Infof("Using interface %v with current breakout mode %v, new breakout mode: %v", port, currBreakoutMode, breakoutMode) + return &RandomPortBreakoutInfo{ + PortName: port, + CurrBreakoutMode: currBreakoutMode, + SupportedBreakoutMode: breakoutMode, + }, + nil +} + +// PhysicalPort returns the physical port corresponding to the given interface. +func PhysicalPort(t *testing.T, dut *ondatra.DUTDevice, interfaceName string) (string, error) { + t.Helper() + portToIndexMap, err := FrontPanelPortToIndexMappingForDevice(t, dut) + if err != nil { + return "", errors.Wrap(err, "failed to fetch front panel port to index mapping") + } + if _, ok := portToIndexMap[interfaceName]; !ok { + return "", errors.Errorf("no entry found for interface %v in front panel port list", interfaceName) + } + return "1/" + strconv.Itoa(portToIndexMap[interfaceName]), nil +} + +// BreakoutStateInfoForPort returns the state values of physical channels and operational status information for ports in a given breakout mode. +func BreakoutStateInfoForPort(t *testing.T, dut *ondatra.DUTDevice, port string, currBreakoutMode string) (map[string]*PortBreakoutInfo, error) { + t.Helper() + // Get list of interfaces for breakout mode. + portInfo, err := ExpectedPortInfoForBreakoutMode(t, dut, port, currBreakoutMode) + if err != nil { + return nil, errors.Wrapf(err, "failed to get expected port information for breakout mode (%v) for port %v", currBreakoutMode, port) + } + if portInfo == nil { + return nil, errors.Errorf("got empty port information for breakout mode %v for port %v", currBreakoutMode, port) + } + // Get physical channels and operational statuses for list of ports in given breakout mode. + for p := range portInfo { + physicalChannels := testhelperIntfPhysicalChannelsGet(t, dut, p) + operStatus := testhelperIntfOperStatusGet(t, dut, p) + portSpeed := testhelperStatePortSpeedGet(t, dut, p) + portInfo[p] = &PortBreakoutInfo{physicalChannels, operStatus, portSpeed} + } + return portInfo, nil +} + +// WaitForInterfaceState polls interface oper-status until it matches the expected oper-status. +func WaitForInterfaceState(t *testing.T, dut *ondatra.DUTDevice, intfName string, expectedOperSatus oc.E_Interface_OperStatus, timeout time.Duration) error { + t.Helper() + // Verify oper-status by polling interface oper-status. + var got oc.E_Interface_OperStatus + for start := time.Now(); time.Since(start) < timeout; { + if got = testhelperIntfOperStatusGet(t, dut, intfName); got == expectedOperSatus { + return nil + } + time.Sleep(100 * time.Millisecond) + } + return errors.Errorf("port oper-status match failed for port %v. got: %v, want: %v", intfName, got, expectedOperSatus) +} + +// TransceiverNumberForPort fetches the transceiver corresponding to the port. +func TransceiverNumberForPort(t *testing.T, dut *ondatra.DUTDevice, port string) (int, error) { + if !IsFrontPanelPort(port) { + return 0, errors.Errorf("port: %v is not a front panel port", port) + } + + // Hardware port is of the format 1/X, where X represents the + // transceiver number. + prefix := "1/" + hardwarePort := testhelperIntfHardwarePortGet(t, dut, port) + if !strings.HasPrefix(hardwarePort, prefix) { + return 0, errors.Errorf("invalid hardware-port: %v for port: %v. It must start with %v", hardwarePort, port, prefix) + } + transceiver, err := strconv.Atoi(strings.TrimPrefix(hardwarePort, prefix)) + if err != nil { + return 0, errors.Wrapf(err, "unable to convert %v to integer for port: %v", strings.TrimPrefix(hardwarePort, prefix), port) + } + return transceiver, nil +} + +// IsParentPort returns whether the specified port is a parent port or not. +func IsParentPort(t *testing.T, dut *ondatra.DUTDevice, port string) (bool, error) { + if !IsFrontPanelPort(port) { + return false, errors.Errorf("port: %v is not a front panel port", port) + } + + slotPortLane, err := slotPortLaneForPort(port) + if err != nil { + return false, err + } + currLaneNumber, err := strconv.Atoi(slotPortLane[laneIndex]) + if err != nil { + return false, errors.Wrapf(err, "failed to convert lane number (%v) to int", currLaneNumber) + } + // Lane number for a parent port is always 1. + return currLaneNumber == 1, nil +} + +// ParentPortNumber returns the port number of the parent of the port. +func ParentPortNumber(port string) (string, error) { + slotPortLane, err := slotPortLaneForPort(port) + if err != nil { + return "", err + } + return slotPortLane[portIndex], nil +} + +// PortPMDFromModel returns the port pmdtype from the model. +func PortPMDFromModel(t *testing.T, dut *ondatra.DUTDevice, port string) (string, error) { + return testhelperPortPmdTypeGet(t, dut, port) +} + +//Adding function to manually convert string to uint32 +func lower(c byte) byte { + return c | ('x' - 'X') +} +func conversionTouint32(s string)(uint32,error) { + maxVal := ^uint32(0) + fmt.Printf("maxval: %d",maxVal) + if s == "" { + return 0,errors.New("StringConvUint32:StringIsNill") + } + var n uint32 + for _, c := range []byte(s) { + var d byte + switch { + case '0' <= c && c <= '9': + d = c - '0' + case 'a' <= lower(c) && lower(c) <= 'z': + d = lower(c) - 'a' + 10 + default: + return 0,errors.New("StringConvUint32:switcherror") + } + if d >= byte(10) { + return 0,errors.New("StringConvUint32:ByteError") + } + if n >= maxVal { + // n*base overflows + return 0,errors.New("StringConvUint32:MaxValueRangeError") + } + n *= uint32(10) + n1 := n + uint32(d) + if n1 < n || n1 > maxVal { + // n+d overflows + return 0,errors.New("StringConvUint32:RangeError") + } + n = n1 + } + return n,nil + +} + +//Adding Function to manually conver string to uint8 +func portStringToUint8(portStr string) (uint8, error) { + var result uint8 + for i := 0; i < len(portStr); i++ { + if portStr[i] < '0' || portStr[i] > '9' { + return 0, fmt.Errorf("invalid character '%c' in port number", portStr[i]) + } + result = result*10 + uint8(portStr[i]-'0') + } + if result > math.MaxUint8 { + return 0, fmt.Errorf("port number %d is out of uint8 range", result) + } + return result, nil +} diff --git a/infrastructure/testhelper/results.go b/infrastructure/testhelper/results.go new file mode 100644 index 0000000..0c0f3fb --- /dev/null +++ b/infrastructure/testhelper/results.go @@ -0,0 +1,14 @@ +package testhelper + +import ( + "testing" +) + +// Teardown performs the teardown routine after the test completion. +func (o TearDownOptions) Teardown(t *testing.T) { + if t.Failed() { + if o.SaveLogs != nil { + o.SaveLogs(t, t.Name()+"_log", o.DUTDeviceInfo, o.DUTPeerDeviceInfo) + } + } +} diff --git a/infrastructure/testhelper/ssh.go b/infrastructure/testhelper/ssh.go new file mode 100644 index 0000000..a8a7ca3 --- /dev/null +++ b/infrastructure/testhelper/ssh.go @@ -0,0 +1,140 @@ +package testhelper + +import ( + "fmt" + "time" + "net" + + "os" + + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" +) + +const ( + sshPort = 22 + sshUser = "root" + defaultTimeout = 30 * time.Second +) + +// Function pointers that interact with the switch or the host. +// They enable unit testing of methods that interact with the switch or the host. +var ( + switchstackPrivateSSHRsaKey = func() (string, error) { + + b, err := os.ReadFile("/home/user/.ssh/key") + return string(b), err + } + + testhelperSSHDial = func(addr string, config *ssh.ClientConfig) (*ssh.Client, error) { + sshClient, err := ssh.Dial("tcp", fmt.Sprintf("[%s]:%d", addr, sshPort), config) + if err != nil { + return nil, WrapError(err, "failure to dial ssh") + } + return sshClient, nil + } + + testhelperNewSSHSession = func(sshClient *ssh.Client) (*ssh.Session, error) { + sshSession, err := sshClient.NewSession() + if err != nil { + return nil, WrapError(err, "failure to create ssh session") + } + return sshSession, nil + } + + testhelperNewSFTPClient = func(sshClient *ssh.Client) (*sftp.Client, error) { + sftpClient, err := sftp.NewClient(sshClient) + if err != nil { + return nil, WrapError(err, "failure to create sftp client") + } + return sftpClient, nil + } + + testhelperCloseSSHClient = func(sshClient *ssh.Client) error { + return sshClient.Close() + } + + testhelperCloseSSHSession = func(sshSession *ssh.Session) error { + return sshSession.Close() + } + + testhelperCloseSFTPClient = func(sftpClient *sftp.Client) error { + return sftpClient.Close() + } + + testhelperOutputSSHSession = func(sshSession *ssh.Session, cmd string) ([]byte, error) { + return sshSession.Output(cmd) + } +) + +// SSHManager provides two ssh objects: ssh.Session and sftp.Client. +type SSHManager struct { + sshClient *ssh.Client + SSHSession *ssh.Session + SFTPClient *sftp.Client +} + +// NewSSHManager returns a new SSHManager, which contains two ssh objects that can help in ssh & scp. +func NewSSHManager(addr string) (*SSHManager, error) { + manager := &SSHManager{} + privKey, err := switchstackPrivateSSHRsaKey() + if err != nil { + return nil, WrapError(err, "failure to fetch ssh key") + } + signer, err := ssh.ParsePrivateKey([]byte(privKey)) + if err != nil { + return nil, WrapError(err, "failure to parse ssh key") + } + authMethod := ssh.PublicKeys(signer) + config := &ssh.ClientConfig{ + User: sshUser, + Auth: []ssh.AuthMethod{authMethod}, + HostKeyCallback: customInsecureIgnoreHostKey, + Timeout: defaultTimeout, + } + if manager.sshClient, err = testhelperSSHDial(addr, config); err != nil { + return nil, err + } + if manager.SSHSession, err = testhelperNewSSHSession(manager.sshClient); err != nil { + return nil, err + } + if manager.SFTPClient, err = testhelperNewSFTPClient(manager.sshClient); err != nil { + return nil, err + } + + return manager, nil +} +// Adding a coustom InsecureIgnoreHostKey to effectivly ignore host key verification +func customInsecureIgnoreHostKey(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil +} +// Close must be called to close the SSHManager. +func (s *SSHManager) Close() error { + var err error + if e := testhelperCloseSSHSession(s.SSHSession); e != nil { + err = WrapError(e, "failure in closing ssh.Session") + } + if e := testhelperCloseSFTPClient(s.SFTPClient); e != nil { + err = WrapError(e, "failure in closing sftp.Client") + } + if e := testhelperCloseSSHClient(s.sshClient); e != nil { + err = WrapError(e, "failure in closing ssh.Client") + } + return err +} + +// RunSSH runs a single SSH command on the device and returns its standard output. +// Handles the creation and closing of SSHManager, since the underlying SSHSession can only call one +// command. +func RunSSH(addr string, cmd string) (string, error) { + m, err := NewSSHManager(addr) + if err != nil { + return "", fmt.Errorf("failed to create ssh helper: %w", err) + } + defer m.Close() + o, err := testhelperOutputSSHSession(m.SSHSession, cmd) + if err != nil { + return "", fmt.Errorf("failed to run command '%s', output='%s', error: %w", cmd, string(o), err) + } + return string(o[:]), nil +} diff --git a/infrastructure/testhelper/testhelper.go b/infrastructure/testhelper/testhelper.go new file mode 100644 index 0000000..824fd57 --- /dev/null +++ b/infrastructure/testhelper/testhelper.go @@ -0,0 +1,426 @@ +// Package testhelper contains APIs that help in writing GPINs Ondatra tests. +package testhelper + +import ( + "crypto/rand" + "math/big" + "fmt" + "strings" + "testing" + "time" + + log "github.com/golang/glog" + healthzpb "github.com/openconfig/gnoi/healthz" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/pkg/errors" +) + +var pph portPmdHandler + +// Function pointers that interact with the switch. They enable unit testing +// of methods that interact with the switch. +var ( + testhelperIntfOperStatusGet = func(t *testing.T, d *ondatra.DUTDevice, port string) oc.E_Interface_OperStatus { + return gnmi.Get(t, d, gnmi.OC().Interface(port).OperStatus().State()) + } + + testhelperAllIntfNameGet = func(t *testing.T, d *ondatra.DUTDevice) []string { + return gnmi.GetAll(t, d, gnmi.OC().InterfaceAny().Name().State()) + } + + testhelperDUTNameGet = func(d *ondatra.DUTDevice) string { + return d.Name() + } + + testhelperDUTPortGet = func(t *testing.T, d *ondatra.DUTDevice, id string) *ondatra.Port { + return d.Port(t, id) + } + + testhelperDUTPortsGet = func(d *ondatra.DUTDevice) []*ondatra.Port { + return d.Ports() + } + + testhelperConfigIntfAggregateIDGet = func(t *testing.T, d *ondatra.DUTDevice, port string) string { + return gnmi.Get(t, d, gnmi.OC().Interface(port).Ethernet().AggregateId().Config()) + } + + testhelperIntfAggregateIDReplace = func(t *testing.T, d *ondatra.DUTDevice, port string, ID string) { + gnmi.Replace(t, d, gnmi.OC().Interface(port).Ethernet().AggregateId().Config(), ID) + } + + testhelperIntfPhysicalChannelsGet = func(t *testing.T, d *ondatra.DUTDevice, port string) []uint16 { + return gnmi.Get(t, d, gnmi.OC().Interface(port).PhysicalChannel().State()) + } + + testhelperIntfOperStatusAwait = func(t *testing.T, d *ondatra.DUTDevice, port string, expectedOperSatus oc.E_Interface_OperStatus, timeout time.Duration) (oc.E_Interface_OperStatus, bool) { + predicate := func(val *ygnmi.Value[oc.E_Interface_OperStatus]) bool { + status, present := val.Val() + return present && status == expectedOperSatus + } + lastVal, match := gnmi.Watch(t, d, gnmi.OC().Interface(port).OperStatus().State(), timeout, predicate).Await(t) + lastStatus, _ := lastVal.Val() + return lastStatus, match + } + + testhelperIntfDelete = func(t *testing.T, d *ondatra.DUTDevice, port string) { + gnmi.Delete(t, d, gnmi.OC().Interface(port).Config()) + } + + testhelperIntfLookup = func(t *testing.T, d *ondatra.DUTDevice, port string) *ygnmi.Value[*oc.Interface] { + return gnmi.Lookup(t, d, gnmi.OC().Interface(port).State()) + } + + testhelperIntfHardwarePortGet = func(t *testing.T, d *ondatra.DUTDevice, port string) string { + return gnmi.Get(t, d, gnmi.OC().Interface(port).HardwarePort().State()) + } + + testhelperConfigPortSpeedGet = func(t *testing.T, d *ondatra.DUTDevice, portName string) oc.E_IfEthernet_ETHERNET_SPEED { + return gnmi.Get(t, d, gnmi.OC().Interface(portName).Ethernet().PortSpeed().Config()) + } + + testhelperStatePortSpeedGet = func(t *testing.T, d *ondatra.DUTDevice, portName string) oc.E_IfEthernet_ETHERNET_SPEED { + return gnmi.Get(t, d, gnmi.OC().Interface(portName).Ethernet().PortSpeed().State()) + } + + testhelperOndatraPortNameGet = func(p *ondatra.Port) string { + return p.Name() + } + + testhelperOndatraPortIDGet = func(p *ondatra.Port) string { + return p.ID() + } + + teardownDUTNameGet = func(t *testing.T) string { + return ondatra.DUT(t, "DUT").Name() + } + + teardownDUTDeviceInfoGet = func(t *testing.T) DUTInfo { + dut := ondatra.DUT(t, "DUT") + return DUTInfo{ + name: dut.Name(), + vendor: dut.Vendor(), + } + } + + teardownDUTPeerDeviceInfoGet = func(t *testing.T) DUTInfo { + duts := ondatra.DUTs(t) + if len(duts) <= 1 { + return DUTInfo{} + } + + if peer, ok := duts["CONTROL"]; ok { + return DUTInfo{ + name: peer.Name(), + vendor: peer.Vendor(), + } + } + return DUTInfo{} + } + + teardownDUTHealthzGet = func(t *testing.T) healthzpb.HealthzClient { + return ondatra.DUT(t, "DUT").RawAPIs().GNOI(t).Healthz() + } + + teardownDUTPeerHealthzGet = func(t *testing.T) healthzpb.HealthzClient { + return ondatra.DUT(t, "CONTROL").RawAPIs().GNOI(t).Healthz() + } + + testhelperBreakoutModeGet = func(t *testing.T, d *ondatra.DUTDevice, physicalPort string) *oc.Component_Port_BreakoutMode { + return gnmi.Get(t, d, gnmi.OC().Component(physicalPort).Port().BreakoutMode().State()) + } + + testhelperPortPmdTypeGet = func(t *testing.T, d *ondatra.DUTDevice, port string) (string, error) { + if pph.PortToTransceiver == nil { + pph.PortToTransceiver = make(map[string]string) + } + + xcvr := "" + if pph.PortToTransceiver[port] == "" { + xcvr = PortTransceiver(t, d, port) + if xcvr == "" { + return "", fmt.Errorf("transceiver not found for %v:%v", d.Name(), port) + } + pph.PortToTransceiver[port] = xcvr + } + + pmd := string(EthernetPMD(t, d, xcvr)) + if pmd == "" { + return "", fmt.Errorf("pmd not found for transceiver:%v", xcvr) + } + return pmd, nil + } + + testhelperTransceiverEmpty = func(t *testing.T, d *ondatra.DUTDevice, port string) bool { + return gnmi.Get(t, d, gnmi.OC().Component(port).Empty().State()) + } +) + +// FrontPanelPortPrefix defines prefix string for front panel ports. +const ( + FrontPanelPortPrefix = "Ethernet" +) + +// RandomInterfaceParams contains optional list of parameters than can be passed to RandomInterface(): +// PortList: If passed, only ports in this list must be considered when picking a random interface. +// IsParent: If set, only parent ports must be considered. +// OperDownOk: If set, then operationally down ports can also be picked. +type RandomInterfaceParams struct { + PortList []string + IsParent bool + OperDownOk bool +} + +// OperStatusInfo returns the list of interfaces with the following oper-status: +// 1) UP +// 2) DOWN +// 3) TESTING +// 4) Any other value +type OperStatusInfo struct { + Up []string + Down []string + Testing []string + Invalid []string +} + +// DUTInfo contains dut related info. +type DUTInfo struct { + name string + vendor ondatra.Vendor +} + +// NewDUTInfo creates the DUTInfo structure for a given DUTDevice +func NewDUTInfo(t *testing.T, dut *ondatra.DUTDevice) DUTInfo { + return DUTInfo{ + name: dut.Name(), + vendor: dut.Vendor(), + } +} + +// TearDownOptions consist of the options to be taken into account by the teardown method. +type TearDownOptions struct { + StartTime time.Time + DUTName string + IDs []string + DUTDeviceInfo DUTInfo + DUTPeerDeviceInfo DUTInfo + SaveLogs func(t *testing.T, savePrefix string, dut, peer DUTInfo) +} + +// NewTearDownOptions creates the TearDownOptions structure with default values. +func NewTearDownOptions(t *testing.T) TearDownOptions { + return TearDownOptions{ + StartTime: time.Now(), + DUTName: teardownDUTNameGet(t), + DUTDeviceInfo: teardownDUTDeviceInfoGet(t), + DUTPeerDeviceInfo: teardownDUTPeerDeviceInfoGet(t), + } +} + +// WithID attaches an ID to the test. +func (o TearDownOptions) WithID(id string) TearDownOptions { + o.IDs = append(o.IDs, id) + return o +} + +// WithIDs attaches a list of IDs to the test. +func (o TearDownOptions) WithIDs(ids []string) TearDownOptions { + for _, id := range ids { + o.IDs = append(o.IDs, id) + } + return o +} + +// TearDown provides an interface to implement the teardown routine. +type TearDown interface { + Teardown(t *testing.T) +} + +// infoHandler is a holder for populateInfoHandlers interface. +type infoHandler struct{} + +// RandomInterface picks a random front panel port which is operationally UP. +// Many tests typically need a link that is up, so we'll return +// a randomly selected interface if it is Operationally UP. Options can be passed +// to this method using RandomInterfaceParams struct. +func RandomInterface(t *testing.T, dut *ondatra.DUTDevice, params *RandomInterfaceParams) (string, error) { + // Parse additional parameters + var portList []string + isParent := false + isOperDownOk := false + if params != nil { + portList = params.PortList + isParent = params.IsParent + isOperDownOk = params.OperDownOk + } + + info, err := FetchPortsOperStatus(t, dut, portList...) + if err != nil || info == nil { + return "", errors.Wrap(err, "failed to fetch ports oper-status") + } + + // By default this API considers only operationally UP ports. + interfaces := info.Up + if isOperDownOk { + interfaces = append(interfaces, info.Down...) + } + + if isParent { + // Pick parent port only. + var parentInterfaces []string + for _, intf := range interfaces { + isParentPort, err := IsParentPort(t, dut, intf) + if err != nil { + return "", errors.Wrapf(err, "IsParentPort() failed for port: %v", intf) + } + if isParentPort { + parentInterfaces = append(parentInterfaces, intf) + } + } + interfaces = parentInterfaces + } + + if len(interfaces) == 0 { + if params == nil { + return "", errors.Errorf("no operationally UP interfaces found in %v", testhelperDUTNameGet(dut)) + } + return "", errors.Errorf("no interface found in %v with params: %+v", testhelperDUTNameGet(dut), *params) + } + interfaceLen := int64(len(interfaces)) + max := big.NewInt(interfaceLen) + randomIndex, _ := rand.Int(rand.Reader, max) + s := interfaces[randomIndex.Int64()] + + log.Infof("Using interface %v (%d considered)", s, len(interfaces)) + return s, nil +} + +// FetchPortsOperStatus fetches the oper-status of the specified front +// panel ports. If front panel ports are not specified, then it fetches the +// oper-status for all ports on the device. It returns the list of ports with +// oper-status values present in OperStatusInfo struct. +func FetchPortsOperStatus(t *testing.T, d *ondatra.DUTDevice, ports ...string) (*OperStatusInfo, error) { + if len(ports) == 0 { + var err error + ports, err = FrontPanelPortListForDevice(t, d) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch front panel ports") + } + } + + operStatusInfo := &OperStatusInfo{} + for _, port := range ports { + switch operStatus := testhelperIntfOperStatusGet(t, d, port); operStatus { + case oc.Interface_OperStatus_UP: + operStatusInfo.Up = append(operStatusInfo.Up, port) + case oc.Interface_OperStatus_DOWN: + operStatusInfo.Down = append(operStatusInfo.Down, port) + case oc.Interface_OperStatus_TESTING: + operStatusInfo.Testing = append(operStatusInfo.Testing, port) + default: + operStatusInfo.Invalid = append(operStatusInfo.Invalid, port) + } + } + + return operStatusInfo, nil +} + +// VerifyPortsOperStatus verifies that the oper-status of the specified front +// panel ports is up. If front panel ports are not specified, then it verifies +// the oper-status for all ports on the device. +func VerifyPortsOperStatus(t *testing.T, d *ondatra.DUTDevice, ports ...string) error { + i, err := FetchPortsOperStatus(t, d, ports...) + if err != nil { + return errors.Wrap(err, "failed to fetch ports oper-status") + } + if len(i.Down) > 0 || len(i.Testing) > 0 || len(i.Invalid) > 0 { + return errors.Errorf("some interfaces are not operationally up: %+v", *i) + } + return nil +} + +// IsFrontPanelPort returns true if the specified port is a front panel port. +func IsFrontPanelPort(port string) bool { + return strings.HasPrefix(port, FrontPanelPortPrefix) +} + +// FrontPanelPortListForDevice returns the list of front panel ports on the switch. +func FrontPanelPortListForDevice(t *testing.T, dut *ondatra.DUTDevice) ([]string, error) { + var frontPanelPortList []string + // Filter-out non-front panel ports. + for _, port := range testhelperAllIntfNameGet(t, dut) { + if IsFrontPanelPort(port) { + frontPanelPortList = append(frontPanelPortList, port) + } + } + if len(frontPanelPortList) == 0 { + return nil, errors.New("no front panel port found") + } + + return frontPanelPortList, nil +} + +// Returns platform-specific information. +func platformInfoForDevice(t *testing.T, dut *ondatra.DUTDevice) (*PlatformInfo, error) { + return NewPlatformInfo(t, dut, "default") +} + +// Returns port-specific information. +func portInfoForDevice(t *testing.T, dut *ondatra.DUTDevice) (*PortInfo, error) { + // Populate port properties statically for front panel ports. + return NewPortInfo(t, dut, "default") +} + +// WrapError wraps a new error with new line or creates a new error if +// err == nil. It has been created because errors.Wrapf() returns nil +// if err == nil. +func WrapError(err error, format string, args ...any) error { + format = format + "\n" + if err == nil { + return errors.Errorf(format, args...) + } + return errors.Wrapf(err, format, args...) +} + +// DUTPortNames returns the port names of the DUT. +func DUTPortNames(dut *ondatra.DUTDevice) []string { + var portNames []string + for _, port := range testhelperDUTPortsGet(dut) { + portNames = append(portNames, testhelperOndatraPortNameGet(port)) + } + return portNames +} + +// populatePortPMDInfo provides api to return list of ports with given pmd type from a set of ports. +type populatePortPMDInfo interface { + portsOfPmdType(dutName string, portNames []string, pmdType oc.E_TransportTypes_ETHERNET_PMD_TYPE) ([]string, error) +} + +// portPmdHandler holds the mapping from port names to transceiver. +type portPmdHandler struct { + PortToTransceiver map[string]string +} + +func (p portPmdHandler) portPmdType(dutName string, port string) (oc.E_TransportTypes_ETHERNET_PMD_TYPE, error) { + return oc.TransportTypes_ETHERNET_PMD_TYPE_ETH_UNDEFINED, nil +} + +func (p portPmdHandler) portsOfPmdType(dutName string, portNames []string, pmdType oc.E_TransportTypes_ETHERNET_PMD_TYPE) ([]string, error) { + var ports []string + return ports, nil +} + +// AvailablePortsOfPMDType returns ports with matching PMD type. +func AvailablePortsOfPMDType(t *testing.T, d *ondatra.DUTDevice, pmdType oc.E_TransportTypes_ETHERNET_PMD_TYPE) ([]string, error) { + if pph.PortToTransceiver == nil { + pph.PortToTransceiver = make(map[string]string) + } + for _, port := range DUTPortNames(d) { + if pph.PortToTransceiver[port] == "" { + pph.PortToTransceiver[port] = gnmi.Get(t, d, gnmi.OC().Interface(port).Transceiver().State()) + } + } + return pph.portsOfPmdType(d.Name(), DUTPortNames(d), pmdType) +} diff --git a/pins_deps.bzl b/pins_deps.bzl new file mode 100644 index 0000000..b80cb2e --- /dev/null +++ b/pins_deps.bzl @@ -0,0 +1,203 @@ +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +def pins_deps(): + if not native.existing_rule("com_github_grpc_grpc"): + http_archive( + name = "com_github_grpc_grpc", + url = "https://github.com/grpc/grpc/archive/v1.58.0.zip", + strip_prefix = "grpc-1.58.0", + sha256 = "aa329c7de707a03511c88206ef4483e9346ab6336b6be4378d294060aa7400b3", + patch_args = ["-p1"], + patches = [ + "//:bazel/patches/grpc-001-fix_file_watcher_race_condition.patch", + "//:bazel/patches/grpc-003-fix_go_gazelle_register_toolchain.patch", + ], + ) + if not native.existing_rule("com_google_absl"): + http_archive( + name = "com_google_absl", + url = "https://github.com/abseil/abseil-cpp/archive/20230802.0.tar.gz", + strip_prefix = "abseil-cpp-20230802.0", + sha256 = "59d2976af9d6ecf001a81a35749a6e551a335b949d34918cfade07737b9d93c5", + ) + if not native.existing_rule("com_google_googletest"): + http_archive( + name = "com_google_googletest", + urls = ["https://github.com/google/googletest/archive/release-1.11.0.tar.gz"], + strip_prefix = "googletest-release-1.11.0", + sha256 = "b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5", + ) + if not native.existing_rule("com_google_benchmark"): + http_archive( + name = "com_google_benchmark", + urls = ["https://github.com/google/benchmark/archive/v1.5.4.tar.gz"], + strip_prefix = "benchmark-1.5.4", + sha256 = "e3adf8c98bb38a198822725c0fc6c0ae4711f16fbbf6aeb311d5ad11e5a081b5", + ) + if not native.existing_rule("com_google_protobuf"): + http_archive( + name = "com_google_protobuf", + url = "https://github.com/protocolbuffers/protobuf/archive/refs/tags/v25.1.zip", + strip_prefix = "protobuf-25.1", + sha256 = "eaafa4e19a6619c15df4c30d7213efbfd0f33ad16021cc5f72bbc5d0877346b5", + ) + if not native.existing_rule("com_googlesource_code_re2"): + http_archive( + name = "com_googlesource_code_re2", + url = "https://github.com/google/re2/archive/refs/tags/2023-06-01.tar.gz", + strip_prefix = "re2-2023-06-01", + sha256 = "8b4a8175da7205df2ad02e405a950a02eaa3e3e0840947cd598e92dca453199b", + ) + if not native.existing_rule("com_google_googleapis"): + http_archive( + name = "com_google_googleapis", + url = "https://github.com/googleapis/googleapis/archive/f405c718d60484124808adb7fb5963974d654bb4.zip", + strip_prefix = "googleapis-f405c718d60484124808adb7fb5963974d654bb4", + sha256 = "406b64643eede84ce3e0821a1d01f66eaf6254e79cb9c4f53be9054551935e79", + ) + if not native.existing_rule("com_github_google_glog"): + http_archive( + name = "com_github_google_glog", + url = "https://github.com/google/glog/archive/v0.6.0.tar.gz", + strip_prefix = "glog-0.6.0", + sha256 = "8a83bf982f37bb70825df71a9709fa90ea9f4447fb3c099e1d720a439d88bad6", + ) + if not native.existing_rule("com_github_otg_models"): + http_archive( + name = "com_github_otg_models", + url = "https://github.com/open-traffic-generator/models/archive/refs/tags/v0.12.5.zip", + strip_prefix = "models-0.12.5", + build_file = "@//:bazel/BUILD.otg-models.bazel", + sha256 = "1a63e769f1d7f42c79bc1115babf54acbc44761849a77ac28f47a74567f10090", + ) + + # Needed to make glog happy. + if not native.existing_rule("com_github_gflags_gflags"): + http_archive( + name = "com_github_gflags_gflags", + url = "https://github.com/gflags/gflags/archive/v2.2.2.tar.gz", + strip_prefix = "gflags-2.2.2", + sha256 = "34af2f15cf7367513b352bdcd2493ab14ce43692d2dcd9dfc499492966c64dcf", + ) + if not native.existing_rule("com_github_gnmi"): + http_archive( + name = "com_github_gnmi", + # v0.10.0 release; commit-hash:5473f2ef722ee45c3f26eee3f4a44a7d827e3575. + url = "https://github.com/openconfig/gnmi/archive/refs/tags/v0.10.0.zip", + strip_prefix = "gnmi-0.10.0", + patch_args = ["-p1"], + patches = [ + "//:bazel/patches/gnmi-001-fix_virtual_proto_import.patch", + ], + sha256 = "2231e1cc398a523fa840810fa6fdb8960639f7b91b57bb8f12ed8681e0142a67", + ) + if not native.existing_rule("com_github_gnoi"): + http_archive( + name = "com_github_gnoi", + # Newest commit on main on 2021-11-08. + url = "https://github.com/openconfig/gnoi/archive/1ece8ed91a0d5d283219a99eb4dc6c7eadb8f287.zip", + strip_prefix = "gnoi-1ece8ed91a0d5d283219a99eb4dc6c7eadb8f287", + sha256 = "991ff13a0b28f2cdc2ccb123261e7554d9bcd95c00a127411939a3a8c8a9cc62", + ) + if not native.existing_rule("com_github_p4lang_p4c"): + http_archive( + name = "com_github_p4lang_p4c", + # Newest commit on main on 2023-10-09. + url = "https://github.com/p4lang/p4c/archive/d79e2e8bfa07c7797891d44b7d084910947bf0a7.zip", + strip_prefix = "p4c-d79e2e8bfa07c7797891d44b7d084910947bf0a7", + sha256 = "1fad9b8e96988da76e3ad01c90e99d70fe7db90b3acb7bddf78b603117e857f9", + ) + if not native.existing_rule("com_github_p4lang_p4runtime"): + # We frequently need bleeding-edge, unreleased version of P4Runtime, so we use a commit + # rather than a release. + http_archive( + name = "com_github_p4lang_p4runtime", + # 90553b9 is the newest commit on main as of 2023-10-09. + urls = ["https://github.com/p4lang/p4runtime/archive/f0e9f33818b74f0009daa44160926e568f1eaa4d.zip"], + strip_prefix = "p4runtime-f0e9f33818b74f0009daa44160926e568f1eaa4d/proto", + sha256 = "97b43996ada83484bfa3f9be205d6b6fd75b9ed6985839414ee72110d369cd53", + ) + if not native.existing_rule("com_github_p4lang_p4_constraints"): + http_archive( + name = "com_github_p4lang_p4_constraints", + urls = ["https://github.com/p4lang/p4-constraints/archive/3d5196a793f375ccbe1bf38ae6c49e2e65604f4b.zip"], + strip_prefix = "p4-constraints-3d5196a793f375ccbe1bf38ae6c49e2e65604f4b", + sha256 = "f87d885ebfd6a1bdf02b4c4ba5bf6fb333f90d54561e4d520a8413c8d1fb7beb", + ) + if not native.existing_rule("com_github_nlohmann_json"): + http_archive( + name = "com_github_nlohmann_json", + # JSON for Modern C++ + url = "https://github.com/nlohmann/json/archive/v3.7.3.zip", + strip_prefix = "json-3.7.3", + sha256 = "e109cd4a9d1d463a62f0a81d7c6719ecd780a52fb80a22b901ed5b6fe43fb45b", + build_file_content = """cc_library(name = "nlohmann_json", + visibility = ["//visibility:public"], + hdrs = glob([ + "include/nlohmann/*.hpp", + "include/nlohmann/**/*.hpp", + ]), + includes = ["include"], + )""", + ) + if not native.existing_rule("com_jsoncpp"): + http_archive( + name = "com_jsoncpp", + url = "https://github.com/open-source-parsers/jsoncpp/archive/1.9.4.zip", + strip_prefix = "jsoncpp-1.9.4", + build_file = "@//:bazel/BUILD.jsoncpp.bazel", + sha256 = "6da6cdc026fe042599d9fce7b06ff2c128e8dd6b8b751fca91eb022bce310880", + ) + if not native.existing_rule("com_github_ivmai_cudd"): + http_archive( + name = "com_github_ivmai_cudd", + build_file = "@//:bazel/BUILD.cudd.bazel", + strip_prefix = "cudd-cudd-3.0.0", + sha256 = "5fe145041c594689e6e7cf4cd623d5f2b7c36261708be8c9a72aed72cf67acce", + urls = ["https://github.com/ivmai/cudd/archive/cudd-3.0.0.tar.gz"], + ) + if not native.existing_rule("com_gnu_gmp"): + http_archive( + name = "com_gnu_gmp", + urls = [ + "https://gmplib.org/download/gmp/gmp-6.2.1.tar.xz", + "https://ftp.gnu.org/gnu/gmp/gmp-6.2.1.tar.xz", + ], + strip_prefix = "gmp-6.2.1", + sha256 = "fd4829912cddd12f84181c3451cc752be224643e87fac497b69edddadc49b4f2", + build_file = "@//:bazel/BUILD.gmp.bazel", + ) + if not native.existing_rule("com_github_z3prover_z3"): + http_archive( + name = "com_github_z3prover_z3", + url = "https://github.com/Z3Prover/z3/archive/z3-4.8.12.tar.gz", + strip_prefix = "z3-z3-4.8.12", + sha256 = "e3aaefde68b839299cbc988178529535e66048398f7d083b40c69fe0da55f8b7", + build_file = "@//:bazel/BUILD.z3.bazel", + ) + if not native.existing_rule("rules_foreign_cc"): + http_archive( + name = "rules_foreign_cc", + sha256 = "d54742ffbdc6924f222d2179f0e10e911c5c659c4ae74158e9fe827aad862ac6", + strip_prefix = "rules_foreign_cc-0.2.0", + url = "https://github.com/bazelbuild/rules_foreign_cc/archive/0.2.0.tar.gz", + ) + if not native.existing_rule("rules_proto"): + http_archive( + name = "rules_proto", + urls = [ + "https://github.com/bazelbuild/rules_proto/archive/3f1ab99b718e3e7dd86ebdc49c580aa6a126b1cd.tar.gz", + ], + strip_prefix = "rules_proto-3f1ab99b718e3e7dd86ebdc49c580aa6a126b1cd", + sha256 = "c9cc7f7be05e50ecd64f2b0dc2b9fd6eeb182c9cc55daf87014d605c31548818", + ) + if not native.existing_rule("rules_pkg"): + http_archive( + name = "rules_pkg", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.5.1/rules_pkg-0.5.1.tar.gz", + "https://github.com/bazelbuild/rules_pkg/releases/download/0.5.1/rules_pkg-0.5.1.tar.gz", + ], + sha256 = "a89e203d3cf264e564fcb96b6e06dd70bc0557356eb48400ce4b5d97c2c3720d", + ) diff --git a/testrunner.sh b/testrunner.sh new file mode 100755 index 0000000..20503b8 --- /dev/null +++ b/testrunner.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +test_name='' +push_config_after_test='false' +push_config_before_test='false' +push_config_success='true' + +print_usage() { + printf "Use:\n\t'-t tests/ondatra:test_name' to run the test.\n\t'-b' to push the switch config before the test.\n\t'-a' to push the config after the test.\n" +} + +while getopts 'abt:' arg; do + case "${arg}" in + t) test_name="${OPTARG}" ;; + a) push_config_after_test='true' ;; + b) push_config_before_test='true' ;; + *) print_usage + exit 1 ;; + esac +done + +function push_config() { + bazel test --test_output=streamed tests/ondatra:installation_test --test_strategy=exclusive --cache_test_results=no + if [ $? -ne 0 ]; then + push_config_success='false' + fi +} + +# Pushes the config before/after test. +function run_test() { + if [ -z "$test_name" ]; then + print_usage + exit 1 + fi + + if [ $push_config_before_test = 'true' ]; then + push_config + fi + + if [ $push_config_success = 'true' ]; then + bazel test --test_output=streamed "$test_name" --test_strategy=exclusive --cache_test_results=no --test_timeout=10000000 + fi + + if [ $push_config_after_test = 'true' ]; then + push_config + fi +} + +run_test diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel new file mode 100644 index 0000000..20dc52c --- /dev/null +++ b/tests/BUILD.bazel @@ -0,0 +1,411 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//tests:ondatra_test.bzl", "ondatra_test", "ondatra_test_suite") + +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + + +# Ethernet Counter Tests (two switches) +ondatra_test( + name = "ethcounter_sw_dual_switch_test", + srcs = ["ethcounter_sw_dual_switch_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_pkg_errors//:errors", + ], +) + +# gNMI Features: Long Stress Test +ondatra_test( + name = "gnmi_long_stress_test", + srcs = ["gnmi_long_stress_test.go"], + run_timeout = "500m", + deps = [ + ":gnmi_stress_helper", + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_ondatra//:go_default_library", + ], +) + +go_library( + name = "gnmi_stress_helper", + testonly = 1, + srcs = ["gnmi_helper.go"], + importpath = "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/tests/gnmi_stress_helper", + deps = [ + "//infrastructure/testhelper", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnmi//value", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ygot//ygot", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_protobuf//encoding/prototext", + ], +) + +# Gnoi File tests +ondatra_test( + name = "gnoi_file_test", + srcs = ["gnoi_file_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnoi//file:file_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + ], +) + +#lacp time-out test +ondatra_test( + name = "lacp_timeout_test", + srcs = ["lacp_timeout_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_pkg_errors//:errors", + ], +) + +# Link event damping tests +ondatra_test( + name = "link_event_damping_test", + srcs = ["link_event_damping_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_ygnmi//ygnmi", + "@com_github_pkg_errors//:errors", + "@org_golang_google_grpc//:go_default_library", + ], +) + +#port debug test +ondatra_test( + name = "port_debug_data_test", + srcs = ["port_debug_data_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + ], +) + +# Hardware Platform Component Tests +ondatra_test( + name = "platforms_hardware_component_test", + srcs = ["platforms_hardware_component_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_testt//:testt", + "@com_github_pkg_errors//:errors", + ], +) + +#module reset test +ondatra_test_suite( + name = "module_reset_test", + srcs = ["module_reset_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_gnoi//types:types_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + ], +) + +# Installation Test +ondatra_test( + name = "installation_test", + srcs = ["installation_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//status", + ], +) + +# Inband SW Interface Counter Tests (two switches) +ondatra_test( + name = "inband_sw_interface_dual_switch_test", + srcs = ["inband_sw_interface_dual_switch_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + ], +) + +# gNMI Features: GET Modes +ondatra_test( + name = "gnmi_get_modes_test", + srcs = ["gnmi_get_modes_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_go_cmp//cmp", + "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnmi//value", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ygot//ygot", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_google_protobuf//testing/protocmp", + ], +) + +#Transceiver test +ondatra_test( + name = "transceiver_test", + srcs = ["transceiver_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_ygot//ygot", + "@org_golang_google_grpc//:go_default_library", + ], +) + +# Inband SW Interface Tests +ondatra_test( + name = "inband_sw_interface_test", + srcs = ["inband_sw_interface_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_testt//:testt", + ], +) + +# Software Platform Component Tests +ondatra_test( + name = "platforms_software_component_test", + srcs = ["platforms_software_component_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_testt//:testt", + "@com_github_pkg_errors//:errors", + ], +) + +# Gnoi Reboot tests +ondatra_test( + name = "gnoi_reboot_test", + srcs = ["gnoi_reboot_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ygnmi//ygnmi", + "@com_github_pkg_errors//:errors", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//codes", + "@org_golang_google_grpc//status", + ], +) + +# CPU Tests +ondatra_test( + name = "cpu_sw_single_switch_test", + srcs = ["cpu_sw_single_switch_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + ], +) + +# Ethernet Counter Tests (single switch) +ondatra_test( + name = "ethcounter_sw_single_switch_test", + srcs = ["ethcounter_sw_single_switch_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + ], +) + +# gNMI Features: GET/SET Operations +ondatra_test( + name = "gnmi_set_get_test", + srcs = ["gnmi_set_get_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_go_cmp//cmp", + "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_testt//:testt", + "@com_github_openconfig_ygnmi//ygnmi", + "@com_github_openconfig_ygot//ygot", + "@org_golang_google_protobuf//encoding/prototext", + "@org_golang_x_sync//errgroup", + "@org_golang_google_grpc//:go_default_library", + ], +) + +# LACP tests +ondatra_test( + name = "lacp_test", + srcs = ["lacp_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_golang_glog//:glog", + "@com_github_google_go_cmp//cmp", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_ygnmi//ygnmi", + "@com_github_pkg_errors//:errors", + ], +) + +# gNMI Features: Stress Test +ondatra_test( + name = "z_gnmi_stress_test", + srcs = ["gnmi_stress_test.go"], + run_timeout = "120m", + deps = [ + ":gnmi_stress_helper", + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ygot//ygot", + "@org_golang_google_grpc//:go_default_library", + ], +) + +# System paths tests +ondatra_test( + name = "system_paths_test", + srcs = ["system_paths_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_golang_glog//:glog", + "@com_github_google_go_cmp//cmp", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_pkg_errors//:errors", + ], +) + +# gNMI Features: Wildcard Subscription +ondatra_test( + name = "gnmi_wildcard_subscription_test", + srcs = ["gnmi_wildcard_subscription_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_go_cmp//cmp", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_gnoi//system:system_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_ygnmi//ygnmi", + "@com_github_openconfig_ygot//ygot", + "@com_github_pkg_errors//:errors", + ], +) + +# Management interface tests +ondatra_test( + name = "mgmt_interface_test", + srcs = ["mgmt_interface_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ondatra//gnmi/oc", + "@com_github_openconfig_testt//:testt", + ], +) + +# gNMI Features: SUBSCRIBE Modes +ondatra_test( + name = "gnmi_subscribe_modes_test", + srcs = ["gnmi_subscribe_modes_test.go"], + deps = [ + "//infrastructure/binding:pinsbind", + "//infrastructure/testhelper", + "@com_github_google_go_cmp//cmp", + "@com_github_google_go_cmp//cmp/cmpopts", + "@com_github_openconfig_gnmi//proto/gnmi:gnmi_go_proto", + "@com_github_openconfig_ondatra//:go_default_library", + "@com_github_openconfig_ondatra//gnmi", + "@com_github_openconfig_ygot//ygot", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_protobuf//encoding/prototext", + ], +) diff --git a/tests/cpu_sw_single_switch_test.go b/tests/cpu_sw_single_switch_test.go new file mode 100644 index 0000000..ed98184 --- /dev/null +++ b/tests/cpu_sw_single_switch_test.go @@ -0,0 +1,385 @@ +package cpu_interface_test + +import ( + "net" + "testing" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +var ( + cpuName = "CPU" + pktsPer uint64 = 7 + counterUpdateDelay time.Duration = 10000 * time.Millisecond +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// TestGNMICPUName - Check that the CPU name is the expected value. +func TestGNMICPUName(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("f9c713f4-3b1e-4a08-82ae-8c82746160a4").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Read the name via /state. + stateName := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Name().State()) + + // Verify the information received from the DUT. + if stateName != cpuName { + t.Errorf("CPU state Name is %v, wanted %v", stateName, cpuName) + } + + // Read the name via /config too. + configName := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Name().Config()) + + // Verify the information received from the DUT. + if configName != cpuName { + t.Errorf("CPU config Name is %v, wanted %v", configName, cpuName) + } +} + +// TestGNMICPUType - Check that the CPU type is 6=ethernetCsmacd. +func TestGNMICPUType(t *testing.T) { + // Report results in TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("4d8c458f-10cf-45eb-95d6-90911f05134a").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Read the type via /state. + stateType := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Type().State()) + + // Verify the information received from the DUT. + if stateType != oc.IETFInterfaces_InterfaceType_ethernetCsmacd { + t.Errorf("CPU state Type is %v, wanted %v", stateType, oc.IETFInterfaces_InterfaceType_ethernetCsmacd) + } + + // Read the type via /config. + configType := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Type().Config()) + + // Verify the information received from the DUT + if configType != oc.IETFInterfaces_InterfaceType_ethernetCsmacd { + t.Errorf("CPU config Type is %v, wanted %v", configType, oc.IETFInterfaces_InterfaceType_ethernetCsmacd) + } + + // Verify that changing the value via config works, even if we can't + // set it other than to IETFInterfaces_InterfaceType_ethernetCsmacd + gnmi.Replace(t, dut, gnmi.OC().Interface(cpuName).Type().Config(), configType) + + // Read the type via /state again. + stateType = gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Type().State()) + + // Verify the information received from the DUT. + if stateType != oc.IETFInterfaces_InterfaceType_ethernetCsmacd { + t.Errorf("CPU state Type is %v, wanted %v", stateType, oc.IETFInterfaces_InterfaceType_ethernetCsmacd) + } +} + +// TestGNMICPURole - Check the CPU interface role +// - management should be false +// - CPU should be true +func TestGNMICPURole(t *testing.T) { + // Reports results in TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("202484be-ff32-4aa2-b459-da1a586b1476").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Read management via /state. Note that the config path for + // these doesn't exist since they're read-only. + stateMgmt := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Management().State()) + + // Verify the information received from the DUT + if stateMgmt != false { + t.Errorf("CPU state Management is %v, wanted false", stateMgmt) + } + + // Read the CPU via /state. + stateCPU := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Cpu().State()) + + // Verify the information received from the DUT. + if stateCPU != true { + t.Errorf("CPU state CPU is %v, wanted true", stateCPU) + } +} + +// TestGNMICPUParentPaths - Check the CPU parent paths. +func TestGNMICPUParentPaths(t *testing.T) { + // Reports results in TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("799e156c-0369-4969-b1f2-9c1197603131").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Read the counters via /state. Note that the config path for + // these doesn't exist since they're read-only. The type + // for the return value is "type Interface_Counters struct" + stateCounters := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Counters().State()) + + // Verify the information received from the DUT. + + // CarrierTransitions isn't expected on CPU interface. + if stateCounters.CarrierTransitions != nil && stateCounters.GetCarrierTransitions() != 0 { + t.Errorf("CPU CarrierTransitions is non-zero: %v", stateCounters.GetCarrierTransitions()) + } + + if stateCounters.InBroadcastPkts == nil { + t.Error("CPU BroadcastPkts is nil") + } + + if stateCounters.InDiscards == nil { + t.Error("CPU InDicards is nil") + } + + // Errors on the CPU interface would be unexpected + if stateCounters.InErrors == nil { + t.Error("CPU InErrors is nil") + } + if stateCounters.GetInErrors() != 0 { + t.Errorf("CPU InErrors is non-zero: %v", stateCounters.GetInErrors()) + } + + // FCS errors aren't possible on the CPU interface. + if stateCounters.InFcsErrors == nil { + t.Error("CPU InFcsErrors is nil") + } else { + if stateCounters.GetInFcsErrors() != 0 { + t.Errorf("CPU InFcsErrors is non-zero: %v", stateCounters.GetInFcsErrors()) + } + } + + if stateCounters.InMulticastPkts == nil { + t.Error("CPU InMulticastPkts is nil") + } + + if stateCounters.InOctets == nil { + t.Error("CPU InOctets is nil") + } + + if stateCounters.InPkts == nil { + t.Error("CPU InPkts is nil") + } + + if stateCounters.InUnicastPkts == nil { + t.Error("CPU InUnicastPkts is nil") + } + + if stateCounters.InUnknownProtos == nil { + t.Error("CPU InUnknownProtos is nil") + } + + if stateCounters.LastClear == nil { + t.Error("CPU LastClear is nil") + } + + if stateCounters.OutBroadcastPkts == nil { + t.Error("CPU OutBroadcastPkts is nil") + } + + if stateCounters.OutDiscards == nil { + t.Error("CPU OutDiscards is nil") + } + + if stateCounters.OutErrors == nil { + t.Error("CPU OutErrors is nil") + } + + if stateCounters.OutMulticastPkts == nil { + t.Error("CPU OutMulticastPkts is nil") + } + + if stateCounters.OutOctets == nil { + t.Error("CPU OutOctets is nil") + } + + if stateCounters.OutPkts == nil { + t.Error("CPU OutPkts is nil") + } + + if stateCounters.OutUnicastPkts == nil { + t.Error("CPU OutUnicastPkts is nil") + } + + // Read the parent via /state. Note that the config path for + // this doesn't exist since it is read-only. The type + // for the return value is + // "type OpenconfigInterfaces_Interfaces_Interface_State struct" + stateIntf := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).State()) + + // Verify the information received from the DUT. + + // AdminStatus may not be valid for CPU so allow for both 0 (not set) + // or UP as valid options. + if stateIntf.AdminStatus != oc.Interface_AdminStatus_UNSET && stateIntf.AdminStatus != oc.Interface_AdminStatus_UP { + t.Errorf("CPU AdminStatus is unexpected: %v", stateIntf.AdminStatus) + } + + // Validate that Counters isn't nil. + if stateIntf.Counters == nil { + t.Error("CPU Counters is nil") + } + + // Enabled may not be valid for CPU, allow. + if stateIntf.Enabled != nil && stateIntf.GetEnabled() != true { + t.Error("CPU is not enabled") + } + + // LoopbackMode may not be valid for CPU, allow. + if lpMode := stateIntf.GetLoopbackMode(); lpMode != oc.Interfaces_LoopbackModeType_UNSET && lpMode != oc.Interfaces_LoopbackModeType_NONE { + t.Errorf("CPU LoopbackMode is not valid: got: %v, want: %v", lpMode, oc.Interfaces_LoopbackModeType_NONE) + } + + // MTU may not be valid for CPU, allow. + if stateIntf.Mtu != nil { + if stateIntf.GetMtu() < 1514 || stateIntf.GetMtu() > 9216 { + t.Errorf("CPU MTU is unexpected: %v (expected [1514-9216])", stateIntf.GetMtu()) + } + } + + // Validate the Name. + if stateIntf.Name == nil { + t.Error("CPU Name is nil") + } else { + name := stateIntf.GetName() + if name != cpuName { + t.Errorf("CPU Name is %v", name) + } + } + + // OperStatus may not be valid for CPU, allow nil (unset). + if stateIntf.OperStatus != oc.Interface_OperStatus_UNSET { + if stateIntf.OperStatus != + oc.Interface_OperStatus_UP { + t.Errorf("CPU OperStatus is unexpected: %v", stateIntf.OperStatus) + } + + } + + // Validate the Type. + if stateIntf.Type != oc.IETFInterfaces_InterfaceType_ethernetCsmacd { + t.Errorf("CPU Type is unexpected: %v", stateIntf.Type) + } +} + +// TestGNMICPUInDiscards - Check CPU In-Discards +// Because the systems we're testing on have existing traffic flowing at random +// intervals, we'll run the test a number of times looking for the expected +// changes. If we get a run with the exact counter increments we expect then +// we exit successfully. If we get a run with more changes than expected to +// the counters then we try again up to the limit. +func TestGNMICPUInDiscards(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("c6e2ef27-d893-451b-9f65-db3e742780fd").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + var bad bool + var i int + + // Iterate up to 5 times to get a successful test. + for i = 1; i <= 5; i++ { + t.Logf("\n----- TestGNMICPUInDiscards: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + cpuStruct := gnmi.Get(t, dut, gnmi.OC().Interface(cpuName).Counters().State()) + beforeInDiscards := cpuStruct.GetInDiscards() + beforeOutDiscards := cpuStruct.GetOutDiscards() + beforeInPkts := cpuStruct.GetInPkts() + beforeInErrors := cpuStruct.GetInErrors() + + // Construct a simple IP packet to an address that the switch + // doesn't know how to route + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x02, 0x02, 0x02, 0x02, 0x02, 0x02}, + DstMAC: net.HardwareAddr{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ + Version: 4, + TTL: 0, + Protocol: layers.IPProtocol(0), + SrcIP: net.ParseIP("192.168.0.10").To4(), + DstIP: net.ParseIP("192.168.0.20").To4(), + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth, ip); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + SubmitToIngress: true, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. At 500ms we frequently + // read the counters before they're updated. Even at 1 second + // I have seen counter increases show up on a subsequent + // iteration rather than this one. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + cpuStruct = gnmi.Get(t, dut, gnmi.OC().Interface("CPU").Counters().State()) + afterInDiscards := cpuStruct.GetInDiscards() + afterOutDiscards := cpuStruct.GetOutDiscards() + afterInPkts := cpuStruct.GetInPkts() + afterInErrors := cpuStruct.GetInErrors() + deltaInDiscards := afterInDiscards - beforeInDiscards + deltaOutDiscards := afterOutDiscards - beforeOutDiscards + + if deltaInDiscards != pktsPer { + t.Logf("beforeIndiscards is %d, afterInDiscards is %d", beforeInDiscards, afterInDiscards) + t.Logf("beforeInPkts %d, afterInPkts %d, beforeInErrors %d, afterInErrors %d", + beforeInPkts, afterInPkts, beforeInErrors, afterInErrors) + t.Logf("deltaInDiscards is %d, expected %d", deltaInDiscards, pktsPer) + bad = true + } + if deltaOutDiscards != 0 { + t.Logf("beforeOutdiscards is %d, afterOutDiscards is %d", beforeOutDiscards, afterOutDiscards) + t.Logf("deltaOutDiscards is %d, expected %d", deltaOutDiscards, 0) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMICPUInDiscards: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMICPUInDiscards: SUCCESS after %v Iteration(s) -----\n\n", i) +} diff --git a/tests/ethcounter_sw_dual_switch_test.go b/tests/ethcounter_sw_dual_switch_test.go new file mode 100644 index 0000000..2a81c27 --- /dev/null +++ b/tests/ethcounter_sw_dual_switch_test.go @@ -0,0 +1,306 @@ +package ethcounter_sw_dual_switch_test + +import ( + "net" + "testing" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/pkg/errors" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +// These are the counters we track in these tests. +type counters struct { + inPkts uint64 + outPkts uint64 + inOctets uint64 + outOctets uint64 + inUnicastPkts uint64 + outUnicastPkts uint64 + inMulticastPkts uint64 + outMulticastPkts uint64 + inBroadcastPkts uint64 + outBroadcastPkts uint64 + inErrors uint64 + outErrors uint64 + inDiscards uint64 + outDiscards uint64 + inIPv4Pkts uint64 + outIPv4Pkts uint64 + inIPv6Pkts uint64 + outIPv6Pkts uint64 + inMTUExceeded uint64 + inIPv6Discards uint64 + outIPv6Discards uint64 +} + +var ( + initialMTU uint16 = 9100 + initialLoopback = oc.Interfaces_LoopbackModeType_NONE +) + +const ( + pktsPer uint64 = 7 + counterUpdateDelay = 3000 * time.Millisecond + mtuStateTimeoutInSeconds = 15 +) + +// Helper functions are here. + +// controlPortLinkedToDutPort returns port on control switch that is connected to given port on DUT. +func controlPortLinkedToDUTPort(t *testing.T, dut *ondatra.DUTDevice, control *ondatra.DUTDevice, dutPort string) (string, error) { + t.Helper() + for _, port := range dut.Ports() { + if port.Name() == dutPort { + if control.Port(t, port.ID()) == nil { + return "", errors.Errorf("control port corresponding to dutPort %v not found", dutPort) + } + return control.Port(t, port.ID()).Name(), nil + } + } + return "", errors.Errorf("control port corresponding to dutPort %v not found", dutPort) +} + +// checkInitial validates preconditions before test starts. +func checkInitial(t *testing.T, dut *ondatra.DUTDevice, intf string) { + t.Helper() + + intfPath := gnmi.OC().Interface(intf) + if operStatus := gnmi.Get(t, dut, intfPath.OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + t.Fatalf("%v OperStatus is unexpected: %v", intf, operStatus) + } + + if gnmi.Get(t, dut, intfPath.LoopbackMode().State()) != oc.Interfaces_LoopbackModeType_NONE { + initialLoopback = oc.Interfaces_LoopbackModeType_FACILITY + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_NONE) + gnmi.Await(t, dut, intfPath.LoopbackMode().State(), 5*time.Second, oc.Interfaces_LoopbackModeType_NONE) + } + + // Read the initial MTU to restore at test end. + initialMTU = gnmi.Get(t, dut, intfPath.Mtu().State()) +} + +// restoreInitial restores the initial conditions at the end of the test. +// +// This routine is called, deferred, at the start of the test to restore +// any conditions tests in this file might modify. +func restoreInitial(t *testing.T, dut *ondatra.DUTDevice, intf string) { + t.Helper() + intfPath := gnmi.OC().Interface(intf) + + // Set loopback mode to false in case we changed it. + if loopbackMode := gnmi.Get(t, dut, intfPath.LoopbackMode().State()); loopbackMode != initialLoopback { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), initialLoopback) + gnmi.Await(t, dut, intfPath.LoopbackMode().State(), 5*time.Second, initialLoopback) + } + + // Restore the initial value of the MTU on the port. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), initialMTU) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), mtuStateTimeoutInSeconds*time.Second, initialMTU) +} + +// readCounters reads all the counters via GNMI and returns a Counters struct. +func readCounters(t *testing.T, dut *ondatra.DUTDevice, intf string) counters { + t.Helper() + + c := counters{} + cntStruct := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().State()) + subPath := gnmi.OC().Interface(intf).Subinterface(0) + ip4Struct := gnmi.Get(t, dut, subPath.Ipv4().Counters().State()) + ip6Struct := gnmi.Get(t, dut, subPath.Ipv6().Counters().State()) + c.inPkts = cntStruct.GetInPkts() + c.outPkts = cntStruct.GetOutPkts() + c.inOctets = cntStruct.GetInOctets() + c.outOctets = cntStruct.GetOutOctets() + c.inUnicastPkts = cntStruct.GetInUnicastPkts() + c.outUnicastPkts = cntStruct.GetOutUnicastPkts() + c.inMulticastPkts = cntStruct.GetInMulticastPkts() + c.outMulticastPkts = cntStruct.GetOutMulticastPkts() + c.inBroadcastPkts = cntStruct.GetInBroadcastPkts() + c.outBroadcastPkts = cntStruct.GetOutBroadcastPkts() + c.inErrors = cntStruct.GetInErrors() + c.outErrors = cntStruct.GetOutErrors() + c.inDiscards = cntStruct.GetInDiscards() + c.outDiscards = cntStruct.GetOutDiscards() + c.inIPv4Pkts = ip4Struct.GetInPkts() + c.outIPv4Pkts = ip4Struct.GetOutPkts() + c.inIPv6Pkts = ip6Struct.GetInPkts() + c.outIPv6Pkts = ip6Struct.GetOutPkts() + c.inIPv6Discards = ip6Struct.GetInDiscardedPkts() + c.outIPv6Discards = ip6Struct.GetOutDiscardedPkts() + c.inMTUExceeded = gnmi.Get(t, dut, gnmi.OC().Interface(intf).Ethernet().Counters().InMaxsizeExceeded().State()) + + return c +} + +// showCountersDelta shows debug info after an unexpected change in counters. +func showCountersDelta(t *testing.T, before counters, after counters, expect counters) { + t.Helper() + + for _, s := range []struct { + desc string + before, after, expect uint64 + }{ + {"in-pkts", before.inPkts, after.inPkts, expect.inPkts}, + {"out-pkts", before.outPkts, after.outPkts, expect.outPkts}, + {"in-octets", before.inOctets, after.inOctets, expect.inOctets}, + {"out-octets", before.outOctets, after.outOctets, expect.outOctets}, + {"in-unicast-pkts", before.inUnicastPkts, after.inUnicastPkts, expect.inUnicastPkts}, + {"out-unicast-pkts", before.outUnicastPkts, after.outUnicastPkts, expect.outUnicastPkts}, + {"in-multicast-pkts", before.inMulticastPkts, after.inMulticastPkts, expect.inMulticastPkts}, + {"out-multicast-pkts", before.outMulticastPkts, after.outMulticastPkts, expect.outMulticastPkts}, + {"in-broadcast-pkts", before.inBroadcastPkts, after.inBroadcastPkts, expect.inBroadcastPkts}, + {"out-broadcast-pkts", before.outBroadcastPkts, after.outBroadcastPkts, expect.outBroadcastPkts}, + {"in-errors", before.inErrors, after.inErrors, expect.inErrors}, + {"out-errors", before.outErrors, after.outErrors, expect.outErrors}, + {"in-mtu-exceeded", before.inMTUExceeded, after.inMTUExceeded, expect.inMTUExceeded}, + {"in-discards", before.inDiscards, after.inDiscards, expect.inDiscards}, + {"out-discards", before.outDiscards, after.outDiscards, expect.outDiscards}, + {"in-ipv4--pkts", before.inIPv4Pkts, after.inIPv4Pkts, expect.inIPv4Pkts}, + {"out-ipv4-pkts", before.outIPv4Pkts, after.outIPv4Pkts, expect.outIPv4Pkts}, + {"in-ipv6-pkts", before.inIPv6Pkts, after.inIPv6Pkts, expect.inIPv6Pkts}, + {"out-ipv6-pkts", before.outIPv6Pkts, after.outIPv6Pkts, expect.outIPv6Pkts}, + {"in-ipv6-discards", before.inIPv6Discards, after.inIPv6Discards, expect.inIPv6Discards}, + {"out-ipv6-discards", before.outIPv6Discards, after.outIPv6Discards, expect.outIPv6Discards}, + } { + if s.before != s.after || s.expect != s.before { + t.Logf("%v %d -> %d expected %d (%+d)", s.desc, s.before, s.after, s.expect, s.after-s.before) + } + } +} + +// Tests start here. +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// TestGNMIEthernetInErrors - Check EthernetX In-Errors +func TestGNMIEthernetInErrors(t *testing.T) { + t.Logf("\n\n\n\n\n----- TestGNMIEthernetInErrors -----\n\n\n\n\n\n") + + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("4b8a5e02-7389-474a-a677-efde088667b0").WithID("fb11ecf4-6e74-4255-b150-4a30c2493c86").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + control := ondatra.DUT(t, "CONTROL") + + params := testhelper.RandomInterfaceParams{ + PortList: []string{ + dut.Port(t, "port1").Name(), + dut.Port(t, "port2").Name(), + dut.Port(t, "port3").Name(), + dut.Port(t, "port4").Name(), + }} + + dutIntf, err := testhelper.RandomInterface(t, dut, ¶ms) + if err != nil { + t.Fatalf("Failed to fetch random interface on DUT: %v", err) + } + + // Get the corresponding interface on control switch. + controlIntf, err := controlPortLinkedToDUTPort(t, dut, control, dutIntf) + if err != nil { + t.Fatalf("Failed to get control port corresponding to DUT port %v: %v", dutIntf, err) + } + + t.Logf("\n\nChose: dut %v control %v\n\n", dutIntf, controlIntf) + + // Check the initial state for this port on both switches. + checkInitial(t, dut, dutIntf) + defer restoreInitial(t, dut, dutIntf) + checkInitial(t, control, controlIntf) + defer restoreInitial(t, control, controlIntf) + + // Set the MTU for the dut switch port to 1500. + var mtu uint16 = 1500 + gnmi.Replace(t, dut, gnmi.OC().Interface(dutIntf).Mtu().Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(dutIntf).Mtu().State(), mtuStateTimeoutInSeconds*time.Second, mtu) + + bad := false + i := 0 + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInErrors: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := readCounters(t, dut, dutIntf) + + // Compute the expected counters after the test. + expect := before + expect.inErrors += pktsPer + expect.inOctets += pktsPer * 2018 + expect.inMTUExceeded += pktsPer + + // Construct a simple oversize unicast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + data := make([]byte, 2000) + for i := range data { + data[i] = 0xfe + } + payload := gopacket.Payload(data) + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth, payload); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: controlIntf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, control, control.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. At 500ms we frequently + // read the counters before they're updated. Even at 1 second + // I have seen counter increases show up on a subsequent + // iteration rather than this one. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + if after := readCounters(t, dut, dutIntf); after != expect { + showCountersDelta(t, before, after, expect) + if after != expect { + bad = true + } + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInErrors: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInErrors: SUCCESS after %v Iteration(s) -----\n\n", i) +} diff --git a/tests/ethcounter_sw_single_switch_test.go b/tests/ethcounter_sw_single_switch_test.go new file mode 100644 index 0000000..a110a2c --- /dev/null +++ b/tests/ethcounter_sw_single_switch_test.go @@ -0,0 +1,1450 @@ +// This file contains all the front panel port counter tests that can be +// done with a single switch, both egress (out) and ingress (in) if they +// can be done in loopback mode. Tests requiring two switches or Ixia +// to accomplish are elsewhere. + +package ethernet_counter_test + +import ( + "net" + "testing" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +// These are the counters we track in these tests. +type Counters struct { + inPkts uint64 + outPkts uint64 + inOctets uint64 + outOctets uint64 + inUnicastPkts uint64 + outUnicastPkts uint64 + inMulticastPkts uint64 + outMulticastPkts uint64 + inBroadcastPkts uint64 + outBroadcastPkts uint64 + inErrors uint64 + outErrors uint64 + inDiscards uint64 + outDiscards uint64 + inMTUExceeded uint64 + inIPv6Discards uint64 + outIPv6Discards uint64 +} + +var ( + initialMTU uint16 = 9100 + pktsPer uint64 = 7 + dmiWriteDelay time.Duration = 250 * time.Millisecond + counterUpdateDelay time.Duration = 5000 * time.Millisecond +) + +const ( + loopbackStateTimeout = 15 * time.Second +) + +// Helper functions are here. +// CheckInitial validates preconditions before test starts. +func CheckInitial(t *testing.T, dut *ondatra.DUTDevice, intf string) { + t.Helper() + + intfPath := gnmi.OC().Interface(intf) + operStatus := gnmi.Get(t, dut, intfPath.OperStatus().State()) + + if operStatus != oc.Interface_OperStatus_UP { + t.Fatalf("%v OperStatus is unexpected: %v", intf, operStatus) + } + + loopbackMode := gnmi.Get(t, dut, intfPath.LoopbackMode().State()) + if loopbackMode != oc.Interfaces_LoopbackModeType_NONE { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_NONE) + gnmi.Await(t, dut, intfPath.LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_NONE) + } + + // Read the initial MTU to restore at test end. + initialMTU = gnmi.Get(t, dut, intfPath.Mtu().State()) +} + +// RestoreInitial restores the initial conditions at the end of the test. +// +// This routine is called, deferred, at the start of the test to restore +// any conditions tests in this file might modify. +func RestoreInitial(t *testing.T, dut *ondatra.DUTDevice, intf string) { + t.Helper() + intfPath := gnmi.OC().Interface(intf) + + // Set loopback mode to false in case we changed it. + loopbackMode := gnmi.Get(t, dut, intfPath.LoopbackMode().State()) + if loopbackMode != oc.Interfaces_LoopbackModeType_NONE { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_NONE) + gnmi.Await(t, dut, intfPath.LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_NONE) + } + + // Restore the initial value of the MTU on the port. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), initialMTU) + got := gnmi.Get(t, dut, intfPath.Mtu().State()) + if got != initialMTU { + t.Fatalf("MTU restore failed! got:%v, want:%v", got, initialMTU) + } +} + +// ReadCounters reads all the counters via GNMI and returns a Counters struct. +func ReadCounters(t *testing.T, dut *ondatra.DUTDevice, intf string) Counters { + t.Helper() + + c := Counters{} + cntStruct := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().State()) + subPath := gnmi.OC().Interface(intf).Subinterface(0) + ip6Struct := gnmi.Get(t, dut, subPath.Ipv6().Counters().State()) + c.inPkts = cntStruct.GetInPkts() + c.outPkts = cntStruct.GetOutPkts() + c.inOctets = cntStruct.GetInOctets() + c.outOctets = cntStruct.GetOutOctets() + c.inUnicastPkts = cntStruct.GetInUnicastPkts() + c.outUnicastPkts = cntStruct.GetOutUnicastPkts() + c.inMulticastPkts = cntStruct.GetInMulticastPkts() + c.outMulticastPkts = cntStruct.GetOutMulticastPkts() + c.inBroadcastPkts = cntStruct.GetInBroadcastPkts() + c.outBroadcastPkts = cntStruct.GetOutBroadcastPkts() + c.inErrors = cntStruct.GetInErrors() + c.outErrors = cntStruct.GetOutErrors() + c.inDiscards = cntStruct.GetInDiscards() + c.outDiscards = cntStruct.GetOutDiscards() + c.inIPv6Discards = ip6Struct.GetInDiscardedPkts() + c.outIPv6Discards = ip6Struct.GetOutDiscardedPkts() + c.inMTUExceeded = gnmi.Get(t, dut, gnmi.OC().Interface(intf).Ethernet().Counters().InMaxsizeExceeded().State()) + + return c +} + +// ShowCountersDelta shows debug info after an unexpected change in counters. +func ShowCountersDelta(t *testing.T, before Counters, after Counters, expect Counters) { + t.Helper() + + for _, s := range []struct { + desc string + before, after, expect uint64 + }{ + {"in-pkts", before.inPkts, after.inPkts, expect.inPkts}, + {"out-pkts", before.outPkts, after.outPkts, expect.outPkts}, + {"in-octets", before.inOctets, after.inOctets, expect.inOctets}, + {"out-octets", before.outOctets, after.outOctets, expect.outOctets}, + {"in-unicast-pkts", before.inUnicastPkts, after.inUnicastPkts, expect.inUnicastPkts}, + {"out-unicast-pkts", before.outUnicastPkts, after.outUnicastPkts, expect.outUnicastPkts}, + {"in-multicast-pkts", before.inMulticastPkts, after.inMulticastPkts, expect.inMulticastPkts}, + {"out-multicast-pkts", before.outMulticastPkts, after.outMulticastPkts, expect.outMulticastPkts}, + {"in-broadcast-pkts", before.inBroadcastPkts, after.inBroadcastPkts, expect.inBroadcastPkts}, + {"out-broadcast-pkts", before.outBroadcastPkts, after.outBroadcastPkts, expect.outBroadcastPkts}, + {"in-errors", before.inErrors, after.inErrors, expect.inErrors}, + {"out-errors", before.outErrors, after.outErrors, expect.outErrors}, + {"in-discards", before.inDiscards, after.inDiscards, expect.inDiscards}, + {"out-discards", before.outDiscards, after.outDiscards, expect.outDiscards}, + {"in-mtu-exceeded", before.inMTUExceeded, after.inMTUExceeded, expect.inMTUExceeded}, + {"in-ipv6-discards", before.inIPv6Discards, after.inIPv6Discards, expect.inIPv6Discards}, + {"out-ipv6-discards", before.outIPv6Discards, after.outIPv6Discards, expect.outIPv6Discards}, + } { + if s.before != s.after || s.expect != s.before { + t.Logf("%v %d -> %d expected %d (%+d)", s.desc, s.before, s.after, s.expect, s.after-s.before) + } + } +} + +// ---------------------------------------------------------------------------- +// Tests start here. +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInterfaceRole - Check EthernetX interface role. +// - management should be false +// - CPU should be false +func TestGNMIEthernetInterfaceRole(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("e0619932-46c8-4e49-9e2b-d79a67d03dea").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + intfPath := gnmi.OC().Interface(intf) + + // Read management via /state. Note that the config path for + // this doesn't exist since it's read-only. + if stateMgmt := gnmi.Get(t, dut, intfPath.Management().State()); stateMgmt { + t.Errorf("%v state Management is %v, wanted false", intf, stateMgmt) + } + + // Read cpu via /state. + if stateCPU := gnmi.Get(t, dut, intfPath.Cpu().State()); stateCPU { + t.Errorf("%v state CPU is %v, wanted false", intf, stateCPU) + } +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthParentPaths - Check EthernetX counters and interface paths. +func TestGNMIEthParentPaths(t *testing.T) { + // Reports results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("1aaa6fc9-da57-4751-89b1-56751ac209c6").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Pick a random interface, EthernetX + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + intfPath := gnmi.OC().Interface(intf) + + // Read all counters via /state. The config path for + // this doesn't exist since it's read-only. The type + // for the return value is "type Interface_Counters struct" + stateCounters := gnmi.Get(t, dut, intfPath.Counters().State()) + + // For most counters, simply require them not to be nil. + if stateCounters.CarrierTransitions == nil { + t.Errorf("%v CarrierTransitions wasn't nil", intf) + } + + if stateCounters.InBroadcastPkts == nil { + t.Errorf("%v BroadcastPkts is nil", intf) + } + + if stateCounters.InDiscards == nil { + t.Errorf("%v InDicards is nil", intf) + } + + if stateCounters.InErrors == nil { + t.Errorf("%v InErrors is nil", intf) + } + + if stateCounters.InFcsErrors == nil { + t.Errorf("%v InFcsErrors is nil", intf) + } + + if stateCounters.InMulticastPkts == nil { + t.Errorf("%v InMulticastPkts is nil", intf) + } + + if stateCounters.InOctets == nil { + t.Errorf("%v InOctets is nil", intf) + } + + if stateCounters.InPkts == nil { + t.Errorf("%v InPkts is nil", intf) + } + + if stateCounters.InUnicastPkts == nil { + t.Errorf("%v InUnicastPkts is nil", intf) + } + + if stateCounters.InUnknownProtos == nil { + t.Errorf("%v InUnknownProtos is nil", intf) + } + + if stateCounters.LastClear == nil { + t.Errorf("%v LastClear is nil", intf) + } + + if stateCounters.OutBroadcastPkts == nil { + t.Errorf("%v OutBroadcastPkts is nil", intf) + } + + if stateCounters.OutDiscards == nil { + t.Errorf("%v OutDiscards is nil", intf) + } + + if stateCounters.OutErrors == nil { + t.Errorf("%v OutErrors is nil", intf) + } + + if stateCounters.OutMulticastPkts == nil { + t.Errorf("%v OutMulticastPkts is nil", intf) + } + + if stateCounters.OutOctets == nil { + t.Errorf("%v OutOctets is nil", intf) + } + + if stateCounters.OutPkts == nil { + t.Errorf("%v OutPkts is nil", intf) + } + + if stateCounters.OutUnicastPkts == nil { + t.Errorf("%v OutUnicastPkts is nil", intf) + } + + // Read parent via /state. Note that the config path for + // this doesn't exist since it is read-only. The type + // for the return value is + // "type OpenconfigInterfaces_Interfaces_Interface_State struct" + stateIntf := gnmi.Get(t, dut, intfPath.State()) + + // Verify the information received. + t.Logf("%v AdminStatus is %v", intf, stateIntf.AdminStatus) + + // Validate AdminStatus is UP. + if stateIntf.AdminStatus != oc.Interface_AdminStatus_UP { + t.Errorf("%v AdminStatus is unexpected: %v", intf, stateIntf.AdminStatus) + } + + // Validate Counters is not nil. + if stateIntf.Counters == nil { + t.Errorf("%v Counters is nil", intf) + } + + // Description may not be valid, allow. Typically ''. + if stateIntf.Description != nil { + t.Logf("%v Description is '%v'", intf, stateIntf.GetDescription()) + } + + // Validate Enabled. + if stateIntf.Enabled == nil { + t.Error("Ethernet0 Enabled is nil") + } else { + if !stateIntf.GetEnabled() { + t.Errorf("%v is not enabled", intf) + } + } + + // Ifindex may not be valid, allow. + if stateIntf.Ifindex != nil { + t.Logf("%v Ifindex is %v", intf, stateIntf.GetIfindex()) + } + + // LastChange may not be valid, allow. + if stateIntf.LastChange != nil { + t.Logf("%v LastChange is %v", intf, stateIntf.GetLastChange()) + } + + // Validate the LoopbackMode. + if stateIntf.LoopbackMode == oc.Interfaces_LoopbackModeType_UNSET { + t.Errorf("%v LoopbackMode is unset", intf) + } else { + if stateIntf.GetLoopbackMode() != oc.Interfaces_LoopbackModeType_NONE { + t.Errorf("LoopbackMode is not valid: got: %v, want: %v", stateIntf.GetLoopbackMode(), oc.Interfaces_LoopbackModeType_NONE) + } + } + + // Validate the MTU. + if stateIntf.Mtu == nil { + t.Errorf("%v Mtu is nil", intf) + } else { + if stateIntf.GetMtu() < 1500 || stateIntf.GetMtu() > 9216 { + t.Errorf("%v Mtu is unexpected: %v (expected [1514-9216]", intf, stateIntf.GetMtu()) + } + } + + // Validate the Name. + if stateIntf.Name == nil { + t.Errorf("%v Name is nil", intf) + } else { + if stateIntf.GetName() != intf { + t.Errorf("%v Name is %v", intf, stateIntf.GetName()) + } + } + + // Validate OperStatus. Looks like links are often down in + // current testbed so allow, at least for this test. + if stateIntf.OperStatus != oc.Interface_OperStatus_UP { + t.Logf("%v OperStatus is %v", intf, stateIntf.OperStatus) + } + + // Validate the Type. + if stateIntf.Type != oc.IETFInterfaces_InterfaceType_ethernetCsmacd { + t.Errorf("%v Type is unexpected: %v", intf, stateIntf.Type) + } +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthSubinterfaceIndex - Check EthernetX subinterface index +func TestGNMIEthSubinterfaceIndex(t *testing.T) { + // Reports results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("4a985794-a347-4bed-a50f-29a7f25b514f").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Pick a random interface, EthernetX + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + stateIndex := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Subinterface(0).Index().State()) + + if stateIndex != 0 { + t.Errorf("%v Subinterface Index is unexpected: %d", intf, stateIndex) + } +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetOut - Check EthernetX Out-Pkts, Out-Octets and Out-Unicast-Pkts +// Because the systems we're testing on have existing traffic flowing at random +// intervals, we'll run the test a number of times looking for the expected +// changes. If we get a run with the exact counter increments we expect then +// we exit successfully. If we get a run with more changes than expected to +// the counters then we try again up to the limit. +func TestGNMIEthernetOut(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("c8eb77e8-12fd-44f9-a0fa-784b06d91491").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + + var bad bool + var i int + + // Iterate up to 5 times to get a successful test. + for i = 1; i <= 5; i++ { + t.Logf("\n----- TestGNMIEthernetOut: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outUnicastPkts += pktsPer + + // Construct a simple unicast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. At 500ms we frequently + // read the counters before they're updated. Even at 1 second + // I have seen counter increases show up on a subsequent + // iteration rather than this one. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetOut: FAILED after %v Iterations -----\n\n", i-1) + } + t.Logf("\n\n----- TestGNMIEthernetOut: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetOutMulticast - Check EthernetX Out-Multicast-Pkts +func TestGNMIEthernetOutMulticast(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("a7bb8eb2-eb78-4658-926a-9f053f27adc6").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + + var bad bool + var i int + + // Iterate up to 5 times to get a successful test. + for i = 1; i <= 5; i++ { + t.Logf("\n----- TestGNMIEthernetOutMulticast: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outMulticastPkts += pktsPer + + + // Construct a simple multicast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x01, 0x00, 0x5E, 0xFF, 0xFF, 0xFF}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetOutMulticast: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetOutMulticast: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetOutBroadcast - Check EthernetX Out-Broadcast-Pkts +func TestGNMIEthernetOutBroadcast(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("3ffe7160-82df-4b91-b41a-bc6c582aa237").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + + var bad bool + var i int + + // Iterate up to 5 times to get a successful test. + for i = 1; i <= 5; i++ { + t.Logf("\n----- TestGNMIEthernetOutBroadcast: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outBroadcastPkts += pktsPer + + // Construct a simple broadcast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetOutBroadcast: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetOutBroadcast: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetIn - Check EthernetX In-Pkts, In-Octets and In-Unicast-Pkts +func TestGNMIEthernetIn(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("1c509238-e94b-4dab-aa1b-f47683a5b302").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetIn: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outUnicastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += 64 * pktsPer + expect.inUnicastPkts += pktsPer + + // Construct a simple unicast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + // We're seeing some random discards during testing due to + // existing traffic being discarded in loopback mode so simply + // set up to ignore them. + expect.inDiscards = after.inDiscards + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetIn: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetIn: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInMulticast - Check EthernetX In-Multicast-Pkts +func TestGNMIEthernetInMulticast(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("0b34a2a3-4b30-41cf-a642-634334357cee").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInMulticast: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outMulticastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += 64 * pktsPer + expect.inMulticastPkts += pktsPer + + // Construct a simple multicast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x01, 0x00, 0x5E, 0xFF, 0xFF, 0xFF}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + // We're seeing some random discards during testing due to + // existing traffic being discarded in loopback mode so simply + // set up to ignore them. + expect.inDiscards = after.inDiscards + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInMulticast: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInMulticast: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInBroadcast - Check EthernetX In-Broadcast-Pkts +func TestGNMIEthernetInBroadcast(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("334c1369-b12f-4f73-aec1-effbb0a3fd4b").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInBroadcast: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outBroadcastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += 64 * pktsPer + expect.inBroadcastPkts += pktsPer + + // Construct a simple multicast Ethernet L2 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + EthernetType: layers.EthernetTypeEthernetCTP, + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + // We're seeing some random discards during testing due to + // existing traffic being discarded in loopback mode so simply + // set up to ignore them. + expect.inDiscards = after.inDiscards + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInBroadcast: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInBroadcast: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInIPv4Pkts - Check EthernetX Subinterface IPv4 in-pkts +func TestGNMIEthernetInIPv4(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("8e134557-a159-44ba-9005-e67c7bf8744c").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInIPv4: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outUnicastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += 64 * pktsPer + expect.inUnicastPkts += pktsPer + + // Construct a simple IPv4 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeIPv4, + } + ip := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolTCP, + SrcIP: net.ParseIP("100.0.0.1").To4(), + DstIP: net.ParseIP("200.0.0.1").To4(), + } + tcp := &layers.TCP{ + SrcPort: 10000, + DstPort: 20000, + Seq: 11050, + } + // Required for checksum computation. + tcp.SetNetworkLayerForChecksum(ip) + payload := gopacket.Payload([]byte{'t', 'e', 's', 't'}) + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields based on packet headers. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, eth, ip, tcp, payload); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + // We're seeing some random discards during testing due to + // existing traffic being discarded in loopback mode so simply + // set up to ignore them. + expect.inDiscards = after.inDiscards + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInIPv4: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInIPv4: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInIPv6Pkts - Check EthernetX Subinterface IPv6 in-pkts +func TestGNMIEthernetInIPv6(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("bb5e6b9f-404d-441d-9a0b-a2ecb9785e1a").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInIPv6: Iteration %v -----\n", i) + bad = false + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. + expect := before + expect.outPkts += pktsPer + expect.outOctets += 64 * pktsPer + expect.outUnicastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += 64 * pktsPer + expect.inUnicastPkts += pktsPer + + // Construct a simple IPv6 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeIPv6, + } + ip := &layers.IPv6{ + Version: 6, + HopLimit: 64, + SrcIP: net.ParseIP("2001:db8::1"), + DstIP: net.ParseIP("2001:db8::2"), + NextHeader: layers.IPProtocolICMPv6, + } + icmp := &layers.ICMPv6{ + TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypePacketTooBig, 0), + } + + icmp.SetNetworkLayerForChecksum(ip) + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields based on packet headers. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, eth, ip, icmp); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + // We're seeing some random discards during testing due to + // existing traffic being discarded in loopback mode so simply + // set up to ignore them. + expect.inDiscards = after.inDiscards + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInIPv6: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInIPv6: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInDiscards - Check EthernetX in-discards +func TestGNMIEthernetInDiscards(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("dde5578a-33f2-40b2-a7fa-a978b9ee0a51").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInDiscards: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test. Since + // we're seeing some discard traffic (1 or 2 per second) during + // normal operation on the Ondatra testbeds with loopback + // turned on, setting the number of packets to be sent larger + // so we can actually verify its those packets that we got. + expect := before + expect.outPkts += pktsPer + expect.outOctets += pktsPer * 64 + expect.outUnicastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += pktsPer * 64 + expect.inUnicastPkts += pktsPer + expect.inDiscards += pktsPer + + // Construct a simple IPv4 packet that will get discarded. In + // offline testing, setting the IP protocol field to zero + // worked to cause a discard on ingest. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ + Version: 4, + TTL: 0, + Protocol: layers.IPProtocol(0), + SrcIP: net.ParseIP("100.0.0.1").To4(), + DstIP: net.ParseIP("200.0.0.1").To4(), + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields based on packet headers. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth, ip); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInDiscards: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInDiscards: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// ---------------------------------------------------------------------------- +// TestGNMIEthernetInIPv6Discards - Check EthernetX Subinterface in-ipv6-discards +func TestGNMIEthernetInIPv6Discards(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("2b04e2cb-cce4-43ef-ad42-5cef4dc8f55c").Teardown(t) + + // Select the dut, or device under test. + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + CheckInitial(t, dut, intf) + defer RestoreInitial(t, dut, intf) + + // To get ingress traffic in Ondatra, turn on loopback mode on + // the selected port just for this test. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), oc.Interfaces_LoopbackModeType_FACILITY) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State(), loopbackStateTimeout, oc.Interfaces_LoopbackModeType_FACILITY) + + var bad bool + var i int + + // Iterate up to 10 times to get a successful test. + for i = 1; i <= 10; i++ { + t.Logf("\n----- TestGNMIEthernetInIPv6Discards: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := ReadCounters(t, dut, intf) + + // Compute the expected counters after the test.. Since + // we're seeing some discard traffic (1 or 2 per second) during + // normal operation on the Ondatra testbeds with loopback + // turned on, setting the number of packets to be sent larger + // so we can actually verify its those packets that we got. + expect := before + expect.outPkts += pktsPer + expect.outOctets += pktsPer * 64 + expect.outUnicastPkts += pktsPer + expect.inPkts += pktsPer + expect.inOctets += pktsPer * 64 + expect.inUnicastPkts += pktsPer + expect.inDiscards += pktsPer + expect.inIPv6Discards += pktsPer + + // Construct a simple IPv6 packet that will get discarded. + // Construct a simple IPv6 packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1A, 0x11, 0x17, 0x5F, 0x80}, + EthernetType: layers.EthernetTypeIPv6, + } + + ip := &layers.IPv6{ + Version: 6, + HopLimit: 0, + SrcIP: net.ParseIP("2001:db8::1"), + DstIP: net.ParseIP("2001:db8::2"), + NextHeader: layers.IPProtocol(0), + } + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields based on packet headers. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + + if err := gopacket.SerializeLayers(buf, opts, eth, ip); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, + Count: uint(pktsPer), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + after := ReadCounters(t, dut, intf) + + if after != expect { + ShowCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIEthernetInIPv6Discards: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIEthernetInIPv6Discards: SUCCESS after %v Iteration(s) -----\n\n", i) +} diff --git a/tests/gnmi_get_modes_test.go b/tests/gnmi_get_modes_test.go new file mode 100644 index 0000000..d557672 --- /dev/null +++ b/tests/gnmi_get_modes_test.go @@ -0,0 +1,1115 @@ +package gnmi_get_modes_test + +import ( + "fmt" + "context" + "encoding/json" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/openconfig/gnmi/value" + "github.com/openconfig/ondatra" + "github.com/openconfig/ygot/ygot" + "github.com/openconfig/ondatra/gnmi" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "google.golang.org/grpc" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/encoding/prototext" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +) + +const ( + compStatePath = "/components/component[name=%s]/state" + compFwVerPath = "/components/component[name=%s]/state/firmware-version" + compParentStatePath = "/components/component[name=%s]/state/parent" + intfPath = "/interfaces/interface[name=%s]" + intfMtuPath = "/interfaces/interface[name=%s]/%s/mtu" + intfNamePath = "/interfaces/interface[name=%s]/%s/name" + intfConfigPath = "/interfaces/interface[name=%s]/config" + intfStatePath = "/interfaces/interface[name=%s]/state" + intfCtrsPath = "/interfaces/interface[name=%s]/state/counters" + intfCtrsStatePath = "/interfaces/interface[name=%s]/state/counters/in-octets" + intfMgmtStatePath = "/interfaces/interface[name=%s]/state/management" +) + +var ignorePaths = []string{ + "/gnmi-pathz-policy-counters/paths/path", +} + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +/* + TODO: Refactor the code to remove getDataTypeTest, and have +different table-driven tests for different methods of getDataTypeTest. +*/ +type getDataTypeTest struct { + uuid string + reqPath string + dataType gpb.GetRequest_DataType + wantVal any + wantNotVal string +} + +func TestGNMIGetModes(t *testing.T) { + dut := ondatra.DUT(t, "DUT") + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Check if the switch is responsive with Get API, which will panic if the switch does not return + // state value for specified interface Openconfig path resulting in a test failure. + mtuVal := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().State()) + + testCases := []struct { + name string + function func(*testing.T) + }{ + { + name: "GetConfigTypeConfigLeaf", + function: getDataTypeTest{ + uuid: "89f2834b-9ce2-4347-ad08-aa2e5b44e994", + reqPath: fmt.Sprintf(intfMtuPath, intf, "config"), + dataType: gpb.GetRequest_CONFIG, + wantVal: uint64(mtuVal), + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetConfigTypeConfigSubtree", + function: getDataTypeTest{ + uuid: "7ee2bf60-8f55-4bdd-a56e-e9a7c37c0611", + reqPath: fmt.Sprintf(intfConfigPath, intf), + dataType: gpb.GetRequest_CONFIG, + wantVal: fmt.Sprintf(intfConfigPath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetConfigTypeStateLeaf", + function: getDataTypeTest{ + uuid: "6cd51db0-b1e5-405b-81b9-6d76b28014c3", + reqPath: fmt.Sprintf(intfMtuPath, intf, "state"), + dataType: gpb.GetRequest_CONFIG, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetConfigTypeStateSubtree", + function: getDataTypeTest{ + uuid: "5685f240-67ce-4eaf-a842-0ae5e5ff470b", + reqPath: fmt.Sprintf(intfStatePath, intf), + dataType: gpb.GetRequest_CONFIG, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetConfigTypeOperationalLeaf", + function: getDataTypeTest{ + uuid: "ab3bb6f3-a85b-4f61-a1fd-01b338aab111", + reqPath: fmt.Sprintf(intfCtrsStatePath, intf), + dataType: gpb.GetRequest_CONFIG, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetConfigTypeOperationalSubtree", + function: getDataTypeTest{ + uuid: "93eff08c-3177-4097-90d3-5bcf42756a96", + reqPath: fmt.Sprintf(intfCtrsPath, intf), + dataType: gpb.GetRequest_CONFIG, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetConfigTypeRoot", + function: getDataTypeTest{ + uuid: "88844ea0-328e-4b96-9454-b426d956a7c7", + dataType: gpb.GetRequest_CONFIG, + wantVal: []string{"/config/"}, + wantNotVal: "/state/", + }.dataTypeForRootNonEmpty, + }, + { + name: "GetStateTypeConfigLeaf", + function: getDataTypeTest{ + uuid: "fda99c23-3f28-49e9-9242-f116f633519a", + reqPath: fmt.Sprintf(intfMtuPath, intf, "config"), + dataType: gpb.GetRequest_STATE, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetStateTypeConfigSubtree", + function: getDataTypeTest{ + uuid: "089f5aca-a8ba-4862-9238-96424b67035f", + reqPath: fmt.Sprintf(intfConfigPath, intf), + dataType: gpb.GetRequest_STATE, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetStateTypeStateLeaf", + function: getDataTypeTest{ + uuid: "e091452a-ea18-423d-9c57-f42d8737012b", + reqPath: fmt.Sprintf(intfMtuPath, intf, "state"), + dataType: gpb.GetRequest_STATE, + wantVal: uint64(mtuVal), + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetStateTypeStateSubtree", + function: getDataTypeTest{ + uuid: "310a421e-eb94-4371-b0a4-557136f51ed9", + reqPath: fmt.Sprintf(intfStatePath, intf), + dataType: gpb.GetRequest_STATE, + wantVal: fmt.Sprintf(intfStatePath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetStateTypeOperationalLeaf", + function: getDataTypeTest{ + uuid: "3b0bd721-0778-4f01-b2ac-927c63f023e1", + reqPath: fmt.Sprintf(compParentStatePath, "os0"), + dataType: gpb.GetRequest_STATE, + wantVal: "chassis", + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetStateTypeOperationalSubtree", + function: getDataTypeTest{ + uuid: "3f937fa1-31ae-4338-b3d7-bc7408106040", + reqPath: fmt.Sprintf(intfCtrsPath, intf), + dataType: gpb.GetRequest_STATE, + wantVal: fmt.Sprintf(intfCtrsPath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetStateTypeRoot", + function: getDataTypeTest{ + uuid: "6b876b6c-e589-4611-a9eb-157fd5898e5d", + dataType: gpb.GetRequest_STATE, + wantVal: []string{"/state/"}, + wantNotVal: "/config/", + }.dataTypeForRootNonEmpty, + }, + { + name: "GetOperationalTypeConfigLeaf", + function: getDataTypeTest{ + uuid: "76209f70-069f-4bb0-b2b3-43e17c1ca955", + reqPath: fmt.Sprintf(intfNamePath, intf, "config"), + dataType: gpb.GetRequest_OPERATIONAL, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetOperationalTypeConfigSubtree", + function: getDataTypeTest{ + uuid: "a6bcac89-4cc4-42d9-b1b5-fe8c32389ae4", + reqPath: fmt.Sprintf(intfConfigPath, intf), + dataType: gpb.GetRequest_OPERATIONAL, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetOperationalTypeStateLeaf", + function: getDataTypeTest{ + uuid: "45013a6f-bdda-420d-9c7a-fc23e4946fac", + reqPath: fmt.Sprintf(intfNamePath, intf, "state"), + dataType: gpb.GetRequest_OPERATIONAL, + wantVal: `{}`, + }.dataTypeForPathEmpty, + }, + { + name: "GetOperationalTypeStateSubtree", + function: getDataTypeTest{ + uuid: "ccc00a4c-8c80-4379-bf41-eb5554a3fad0", + reqPath: fmt.Sprintf(intfStatePath, intf), + dataType: gpb.GetRequest_OPERATIONAL, + wantVal: fmt.Sprintf(intfStatePath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetOperationalTypeOperationalLeaf", + function: getDataTypeTest{ + uuid: "c829fd56-7452-4cf1-8a51-8b6ad80ed109", + reqPath: fmt.Sprintf(intfMgmtStatePath, intf), + dataType: gpb.GetRequest_OPERATIONAL, + wantVal: false, + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetOperationalTypeRoot", + function: getDataTypeTest{ + uuid: "1c27789a-de88-4cb4-9b17-6ecc2018423e", + reqPath: "/", + }.operationalUpdateNotInConfigCheck, + }, + { + name: "GetOperationalTypeSubTree", + function: getDataTypeTest{ + uuid: "f65f9291-b695-4f89-b9d8-26443bcd26d1", + reqPath: fmt.Sprintf(intfPath, intf), + }.operationalUpdateNotInConfigCheck, + }, + { + name: "GetAllTypeConfigLeaf", + function: getDataTypeTest{ + uuid: "acbc0dce-9e4c-4005-ae9d-b00e7bfb63ff", + reqPath: fmt.Sprintf(intfMtuPath, intf, "config"), + dataType: gpb.GetRequest_ALL, + wantVal: uint64(mtuVal), + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetAllTypeConfigSubtree", + function: getDataTypeTest{ + uuid: "b565e2cb-de9c-4ea1-99b2-88a63ea93981", + reqPath: fmt.Sprintf(intfConfigPath, intf), + dataType: gpb.GetRequest_ALL, + wantVal: fmt.Sprintf(intfConfigPath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetAllTypeStateLeaf", + function: getDataTypeTest{ + uuid: "f010154c-ef5b-4068-b4aa-c5dc54e02303", + reqPath: fmt.Sprintf(intfMtuPath, intf, "state"), + dataType: gpb.GetRequest_ALL, + wantVal: uint64(mtuVal), + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetAllTypeStateSubtree", + function: getDataTypeTest{ + uuid: "45e1f65e-02d3-4c7c-a1e0-f84ebcc6db93", + reqPath: fmt.Sprintf(intfStatePath, intf), + dataType: gpb.GetRequest_ALL, + wantVal: fmt.Sprintf(intfStatePath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetAllTypeOperationalLeaf", + function: getDataTypeTest{ + uuid: "35961781-c3d6-4c50-9ea2-bb1b9c09a6f2", + reqPath: fmt.Sprintf(compParentStatePath, "os0"), + dataType: gpb.GetRequest_ALL, + wantVal: "chassis", + }.dataTypeForLeafNonEmpty, + }, + { + name: "GetAllTypeOperationalSubtree", + function: getDataTypeTest{ + uuid: "f9e27f17-5bb6-4e04-b07a-181bdb6628b8", + reqPath: fmt.Sprintf(intfStatePath, intf), + dataType: gpb.GetRequest_ALL, + wantVal: fmt.Sprintf(intfStatePath, intf), + }.dataTypeForNonLeafNonEmpty, + }, + { + name: "GetAllTypeRoot", + function: getDataTypeTest{ + uuid: "0993eb99-ea29-485d-88b1-694f030ffa1c", + dataType: gpb.GetRequest_STATE, + wantVal: []string{"/state/", "/config/"}, + }.dataTypeForRootNonEmpty, + }, + { + name: "GetConsistencyConfigLeaf", + function: getDataTypeTest{ + uuid: "13068a17-affc-46dc-a9f4-ef8639d70c8f", + reqPath: fmt.Sprintf(intfMtuPath, intf, "config"), + dataType: gpb.GetRequest_CONFIG, + }.consistencyCheckLeafLevel, + }, + { + name: "GetConsistencyStateLeaf", + function: getDataTypeTest{ + uuid: "1db85b3d-ac1c-422e-a4ea-108a71393773", + reqPath: fmt.Sprintf(intfMtuPath, intf, "state"), + dataType: gpb.GetRequest_STATE, + }.consistencyCheckLeafLevel, + }, + { + name: "GetConsistencyOperationalLeaf", + function: getDataTypeTest{ + uuid: "319cb11c-70e2-4f59-a39c-8e804685728d", + reqPath: fmt.Sprintf(compFwVerPath, "chassis"), + dataType: gpb.GetRequest_OPERATIONAL, + }.consistencyCheckLeafLevel, + }, + { + name: "GetConsistencyConfigSubtree", + function: getDataTypeTest{ + uuid: "036b0a6f-91eb-41ce-9efa-92da9812cc29", + reqPath: fmt.Sprintf(intfConfigPath, intf), + dataType: gpb.GetRequest_CONFIG, + }.consistencyCheckSubtreeLevel, + }, + { + name: "GetConsistencyStateSubtree", + function: getDataTypeTest{ + uuid: "3b215c64-33c7-47b9-8ec5-01f378e09b68", + reqPath: fmt.Sprintf(compStatePath, "os0"), + dataType: gpb.GetRequest_STATE, + }.consistencyCheckSubtreeLevel, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, testCase.function) // Calls the sub-test method. + } +} + +// Helper function to create the Get Request. +func createGetRequest(dut *ondatra.DUTDevice, paths []*gpb.Path, dataType gpb.GetRequest_DataType) *gpb.GetRequest { + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: paths, + Type: dataType, + Encoding: gpb.Encoding_PROTO, + } + return getRequest +} + +// Test for gNMI Get for Data Type for Leaf path when non-empty subtree is returned. +func (c getDataTypeTest) dataTypeForLeafNonEmpty(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Create Get Request. + sPath, err := ygot.StringToStructuredPath(c.reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + getRequest := createGetRequest(dut, paths, c.dataType) + t.Logf("GetRequest:\n%v", getRequest) + + // Send Get request using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + + if getResp == nil { + t.Fatalf("Get response is nil") + } + t.Logf("GetResponse:\n%v", getResp) + + // Validate GET response. + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + notif, updates := notifs[0], notifs[0].GetUpdate() + if len(updates) != 1 { + t.Fatalf("got %d updates in the notification, want 1", len(updates)) + } + pathStr, err := ygot.PathToString(&gpb.Path{Elem: notif.GetPrefix().GetElem()}) + if err != nil { + t.Fatalf("failed to convert elems (%v) to string: %v", notif.GetPrefix().GetElem(), err) + } + + updatePath, err := ygot.PathToString(updates[0].GetPath()) + if err != nil { + t.Fatalf("failed to convert path to string (%v): %v", updatePath, err) + } + gotPath := updatePath + if pathStr != "/" { + gotPath = pathStr + updatePath + } + if gotPath != c.reqPath { + t.Fatalf("got %s path, want %s path", gotPath, c.reqPath) + } + val := updates[0].GetVal() + + var gotVal any + if val.GetJsonIetfVal() == nil { + // Get Scalar value. + gotVal, err = value.ToScalar(val) + if err != nil { + t.Errorf("got %v, want scalar value", gotVal) + } + } else { + // Unmarshal json data to container. + if err := json.Unmarshal(val.GetJsonIetfVal(), &gotVal); err != nil { + t.Fatalf("could not unmarshal json data to container: %v", err) + } + var wantJSONStruct any + if err := json.Unmarshal([]byte(c.wantVal.(string)), &wantJSONStruct); err != nil { + t.Fatalf("could not unmarshal json data to container: %v", err) + } + c.wantVal = wantJSONStruct + } + if !cmp.Equal(gotVal, c.wantVal) { + t.Fatalf("got %v value with type %T, want %v value with type %T", gotVal, gotVal, c.wantVal, c.wantVal) + } +} + +// Test for gNMI Get for Data Type for path when empty subtree is returned. +func (c getDataTypeTest) dataTypeForPathEmpty(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Create Get Request. + sPath, err := ygot.StringToStructuredPath(c.reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + getRequest := createGetRequest(dut, paths, c.dataType) + t.Logf("GetRequest:\n%v", getRequest) + + // Send Get request using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + + if getResp == nil { + t.Fatalf("Get response is nil") + } + t.Logf("GetResponse:\n%v", getResp) + + // Validate GET response. + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + // Expect an empty subtree and zero updates in the notification response. + if updates := notifs[0].GetUpdate(); len(updates) != 0 { + t.Fatalf("Expected 0 updates, got (%v) updates", len(updates)) + } +} + +// Test for gNMI Get for Data Type for non-leaf path when non-empty subtree is returned. +func (c getDataTypeTest) dataTypeForNonLeafNonEmpty(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Create Get Request. + sPath, err := ygot.StringToStructuredPath(c.reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + getRequest := createGetRequest(dut, paths, c.dataType) + t.Logf("GetRequest:\n%v", getRequest) + + // Send Get request using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + t.Logf("GetResponse:\n%v", getResp) + + // Validate GET response. + want, ok := c.wantVal.(string) + if !ok { + t.Fatalf("Error with interface to string conversion (%v)", c.wantVal) + } + + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + notif, updates := notifs[0], notifs[0].GetUpdate() + if len(updates) == 0 { + t.Fatalf("got %d updates in the notification, want >= 1", len(updates)) + } + pathStr, err := ygot.PathToString(&gpb.Path{Elem: notif.GetPrefix().GetElem()}) + if err != nil { + t.Fatalf("failed to convert elems (%v) to string: %v", notif.GetPrefix().GetElem(), err) + } + + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("failed to convert path to string (%v): %v", updatePath, err) + } + fullPath := updatePath + if pathStr != "/" { + fullPath = pathStr + updatePath + } + if !strings.HasPrefix(fullPath, want) { + t.Fatalf("path compare failed to match; got (%v), want prefix (%v)", fullPath, want) + } + } +} + +func containsOneOfTheseSubstrings(haystack string, needles []string) bool { + for i := range needles { + if strings.Contains(haystack, needles[i]) { + return true + } + } + return false +} + +// Test for gNMI Get for Data Type for root path when non-empty subtree is returned. +func (c getDataTypeTest) dataTypeForRootNonEmpty(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + var paths []*gpb.Path + getRequest := createGetRequest(dut, paths, c.dataType) + t.Logf("GetRequest:\n%v", getRequest) + + // Send Get request using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("(%v): Error while calling Get Raw API: (%v)", "dataTypeForRootNonEmpty", err) + } + t.Logf("GetResponse:\n%v", getResp) + + // Validate GET response. + notifs := getResp.GetNotification() + if len(notifs) < 6 { + t.Fatalf("(%v): for path(%v) and type(%v), got %d notifications, want >= 6", + "dataTypeForRootNonEmpty", c.reqPath, c.dataType, len(notifs)) + } + wantVal, ok := c.wantVal.([]string) + if !ok { + t.Fatalf("(%v): Error with interface to map conversion (%v)", "dataTypeForRootNonEmpty", c.wantVal) + } + for u := range notifs { + updates := notifs[u].GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("(%v): failed to convert path (%v) to string (%v): %v", "dataTypeForRootNonEmpty", updatePath, prototext.Format(update), err) + } + if containsOneOfTheseSubstrings(updatePath, ignorePaths) { + continue + } + if !containsOneOfTheseSubstrings(updatePath, wantVal) { + if c.wantNotVal != "" && strings.Contains(updatePath, c.wantNotVal) { + t.Fatalf("(%v): path compare failed to match; got (%v), want contains (%v)", "dataTypeForRootNonEmpty", updatePath, c.wantNotVal) + } + } + } + } +} + +func notificationsFromGetRequest(t *testing.T, dut *ondatra.DUTDevice, getRequest *gpb.GetRequest) ([]*gpb.Notification, error) { + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + return nil, err + } + return getResp.GetNotification(), nil +} + +// Test for gNMI Get for Data Type for root path when non-empty subtree is returned. +func (c getDataTypeTest) operationalUpdateNotInConfigCheck(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + var paths []*gpb.Path + if c.reqPath != "/" { + sPath, err := ygot.StringToStructuredPath(c.reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths = []*gpb.Path{sPath} + } + configNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_CONFIG)) + if err != nil { + t.Fatalf(err.Error()) + } + operNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_OPERATIONAL)) + if err != nil { + t.Fatalf(err.Error()) + } + + if len(operNotifs) < 1 { + t.Fatalf("(%v): for path(%v) and type(%v), got %d notifications, want >= 1", + "operationalForRootNonEmpty", c.reqPath, gpb.GetRequest_OPERATIONAL, len(operNotifs)) + } + + // Build a set from the config updates + configUpdatesSet := make(map[string]bool) + for u := range configNotifs { + updates := configNotifs[u].GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("(%v): failed to convert path (%v) to string (%v): %v", "operationalRootCheck", updatePath, prototext.Format(update), err) + } + configUpdatesSet[updatePath] = true + } + } + + // Check for operational update leaves that have a corresponding config update, none should exist + for u := range operNotifs { + updates := operNotifs[u].GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("(%v): failed to convert path (%v) to string (%v): %v", "operationalRootCheck", updatePath, prototext.Format(update), err) + } + if strings.Contains(updatePath, "\\state\\") { + operPathAsConfig := strings.Replace(updatePath, "\\state\\", "\\config\\", 1) + if _, ok := configUpdatesSet[operPathAsConfig]; ok { + t.Fatalf("(%v): Found operational update with a corresponding config update: (%v)", "operationalRootCheck", updatePath) + } + } + } + } +} + +// Helper function to create and validate the GET request. +func createAndValidateLeafRequest(t *testing.T, dut *ondatra.DUTDevice, paths []*gpb.Path, dataType gpb.GetRequest_DataType, wantPath string) any { + t.Helper() + getReq := createGetRequest(dut, paths, dataType) + // Send Get request using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getReq) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + t.Logf("GetResponse %v for type %v", getResp, dataType) + + // Validate GET response. + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + notif, updates := notifs[0], notifs[0].GetUpdate() + if len(updates) != 1 { + t.Fatalf("got %d updates in the notification, want 1", len(updates)) + } + pathStr, err := ygot.PathToString(&gpb.Path{Elem: notif.GetPrefix().GetElem()}) + if err != nil { + t.Fatalf("failed to convert elems (%v) to string: %v", notif.GetPrefix().GetElem(), err) + } + updatePath, err := ygot.PathToString(updates[0].GetPath()) + if err != nil { + t.Fatalf("failed to convert path to string (%v): %v", updatePath, err) + } + gotPath := updatePath + if pathStr != "/" { + gotPath = pathStr + updatePath + } + if gotPath != wantPath { + t.Fatalf("got %s path, want %s path", gotPath, wantPath) + } + + val := updates[0].GetVal() + var gotVal any + if val.GetJsonIetfVal() == nil { + // Get Scalar value. + gotVal, err = value.ToScalar(val) + if err != nil { + t.Errorf("got %v, want scalar value", gotVal) + } + } else { + // Unmarshal json data to container. + if err := json.Unmarshal(val.GetJsonIetfVal(), &gotVal); err != nil { + t.Fatalf("could not unmarshal json data to container: %v", err) + } + } + return gotVal +} + +// Test for gNMI GET consistency for specified data type with ALL type at leaf level. +func (c getDataTypeTest) consistencyCheckLeafLevel(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + sPath, err := ygot.StringToStructuredPath(c.reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + wantVal := createAndValidateLeafRequest(t, dut, paths, gpb.GetRequest_ALL, c.reqPath) + gotVal := createAndValidateLeafRequest(t, dut, paths, c.dataType, c.reqPath) + if !cmp.Equal(gotVal, wantVal) { + t.Fatalf("(consistencyCheckLeafLevel): got %v value with type %T, want %v value with type %T", gotVal, gotVal, wantVal, wantVal) + } + if c.dataType == gpb.GetRequest_OPERATIONAL { + wantVal = createAndValidateLeafRequest(t, dut, paths, gpb.GetRequest_STATE, c.reqPath) + if !cmp.Equal(gotVal, wantVal) { + t.Fatalf("(consistencyCheckLeafLevel): got %v value with type %T, want %v value with type %T", gotVal, gotVal, wantVal, wantVal) + } + } +} + +// Helper function to create and validate the GET request for subtrees. +func createAndValidateSubtreeRequest(t *testing.T, dut *ondatra.DUTDevice, paths []*gpb.Path, dataType gpb.GetRequest_DataType, wantPath string) []*gpb.Update { + t.Helper() + getReq := createGetRequest(dut, paths, dataType) + // Send Get request using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getReq) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + t.Logf("GetResponse %v for type %v", getResp, dataType) + + // Validate GET response. + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + updates := notifs[0].GetUpdate() + if len(updates) == 0 { + t.Fatalf("got %d updates in the notification, want >= 1", len(updates)) + } + return updates +} + +// Test for gNMI GET consistency for specified data type with ALL type at subtree level. +func (c getDataTypeTest) consistencyCheckSubtreeLevel(t *testing.T) { + t.Helper() + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + sPath, err := ygot.StringToStructuredPath(c.reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + wantVal := createAndValidateSubtreeRequest(t, dut, paths, gpb.GetRequest_ALL, c.reqPath) + gotVal := createAndValidateSubtreeRequest(t, dut, paths, c.dataType, c.reqPath) + sortProtos := cmpopts.SortSlices(func(m1, m2 *gpb.Update) bool { return m1.String() < m2.String() }) + if diff := cmp.Diff(wantVal, gotVal, protocmp.Transform(), sortProtos); diff != "" { + t.Fatalf("(consistencyCheckSubtreeLevel) diff (-want +got):\n%s", diff) + } +} + +// This test exposes an issue with /system/mount-points paths +func TestGetAllEqualsConfigStateOperationalWithRoot(t *testing.T) { + t.Skip("This isn't a tracked test, but it reveals behavior that requires additional investigation") + var paths []*gpb.Path + verifyGetAllEqualsConfigStateOperational(t, "--Not currently a tracked test--", paths) +} + +func TestGetAllEqualsConfigStateOperational(t *testing.T) { + sPath, err := ygot.StringToStructuredPath("/interfaces/") + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + verifyGetAllEqualsConfigStateOperational(t, "f49b3091-97d9-4bf0-b82d-712acf7ffba8", []*gpb.Path{sPath}) +} + +func verifyGetAllEqualsConfigStateOperational(t *testing.T, tid string, paths []*gpb.Path) { + defer testhelper.NewTearDownOptions(t).WithID(tid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + csoPathsSet := make(map[string]bool) + allPathsSet := make(map[string]bool) + + configNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_CONFIG)) + if err != nil { + t.Fatalf(err.Error()) + } + stateNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_STATE)) + if err != nil { + t.Fatalf(err.Error()) + } + operNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_OPERATIONAL)) + if err != nil { + t.Fatalf(err.Error()) + } + allNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_ALL)) + if err != nil { + t.Fatalf(err.Error()) + } + + // Build a set from the config, state, and operational updates + for _, notifs := range [][]*gpb.Notification{configNotifs, stateNotifs, operNotifs} { + for _, notif := range notifs { + updates := notif.GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("(%v): failed to convert path (%v) to string (%v): %v", t.Name(), updatePath, prototext.Format(update), err) + } + csoPathsSet[updatePath] = true + } + } + } + // Build a set from the all updates + for _, notif := range allNotifs { + updates := notif.GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("(%v): failed to convert path (%v) to string (%v): %v", t.Name(), updatePath, prototext.Format(update), err) + } + allPathsSet[updatePath] = true + } + } + + // Check that ALL update leaves that are present in the CSO updates, and vice versa + // Filter out `process` updates as they are too volatile. + var missesFromCSO []string + for path := range allPathsSet { + if _, ok := csoPathsSet[path]; !ok { + missesFromCSO = append(missesFromCSO, path) + } + } + var missesFromAll []string + for path := range csoPathsSet { + if _, ok := allPathsSet[path]; !ok { + missesFromAll = append(missesFromAll, path) + } + } + if len(missesFromCSO) > 0 || len(missesFromAll) > 0 { + t.Fatalf("(%v): Found %v ALL updates missing from CSO updates set:\n%v\n\nFound %v CSO updates missing from ALL updates set:\n%v", t.Name(), len(missesFromCSO), missesFromCSO, len(missesFromAll), missesFromAll) + } +} + +func TestGetConsistencyOperationalSubtree(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b3bc19aa-defe-41be-8344-9ad30460136f").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + sPath, err := ygot.StringToStructuredPath(fmt.Sprintf(compStatePath, "os0")) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + + stateNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_STATE)) + if err != nil { + t.Fatalf(err.Error()) + } + operNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_OPERATIONAL)) + if err != nil { + t.Fatalf(err.Error()) + } + allNotifs, err := notificationsFromGetRequest(t, dut, createGetRequest(dut, paths, gpb.GetRequest_ALL)) + if err != nil { + t.Fatalf(err.Error()) + } + + // Build sets from both the STATE and ALL notifications + updateSetSlice := make([]map[string]bool, 2) + for i, notifs := range [][]*gpb.Notification{stateNotifs, allNotifs} { + updateSetSlice[i] = make(map[string]bool) + for _, notif := range notifs { + updates := notif.GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updateSetSlice[i][update.String()] = true + } + } + } + // Confirm that every OPERATIONAL update is present in both STATE/ALL updates + var misses []string + for i := range updateSetSlice { + for _, notif := range operNotifs { + updates := notif.GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + if _, ok := updateSetSlice[i][update.String()]; !ok { + misses = append(misses, update.String()) + } + } + } + } + if len(misses) > 0 { + t.Fatalf("(%v): Found %v OPER updates missing:\n%v", t.Name(), len(misses), misses) + } +} + +func TestGetInvalidLeaves(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("7e81cbdf-a113-47a4-851c-1df917646c01").Teardown(t) + dut := ondatra.DUT(t, "DUT") + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + types := []gpb.GetRequest_DataType{gpb.GetRequest_STATE, gpb.GetRequest_CONFIG, gpb.GetRequest_OPERATIONAL, gpb.GetRequest_ALL} + invalidPaths := []string{ + "/interfaces/interface[name=%s]/config/fake-leaf", + "/interfaces/interface[name=%s]/state/fake-leaf", + "/interfaces/interface[name=%s]/state/counters/fake-counter", + "/interfaces/interface[name=%s]/state/fake-leaf"} + if len(types) != len(invalidPaths) { + t.Fatalf("types and invalidPaths should be the same size") + } + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + for i := range invalidPaths { + sPath, err := ygot.StringToStructuredPath(fmt.Sprintf(invalidPaths[i], intf)) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + if _, err := gnmiClient.Get(ctx, createGetRequest(dut, paths, types[i])); err == nil { + t.Fatalf("Expected an error with this invalid path(%v)", invalidPaths[i]) + } + } +} + +func TestGetInvalidTypesReturnError(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("5a46b9d1-9f9a-4567-a852-93eb2548f3f6").Teardown(t) + dut := ondatra.DUT(t, "DUT") + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + types := []gpb.GetRequest_DataType{4, 5, 6, 7} + validPaths := []string{ + "/interfaces/interface[name=%s]/config", + "/interfaces/interface[name=%s]/state", + "/interfaces/interface[name=%s]/state/counters", + "/interfaces/interface[name=%s]/state"} + if len(types) != len(validPaths) { + t.Fatalf("types and invalidPaths should be the same size") + } + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + for i := range validPaths { + path := fmt.Sprintf(validPaths[i], intf) + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + + if _, err := gnmiClient.Get(ctx, createGetRequest(dut, paths, types[i])); err == nil { + t.Fatalf("No error received for Get with invalid type. Expected an error with invalid type (%v) for path (%v)", types[i], path) + } + } +} + +func TestMissingTypeAssumesAll(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("1f3f5692-47a6-4c47-ac05-96d705752883").Teardown(t) + dut := ondatra.DUT(t, "DUT") + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + types := []gpb.GetRequest_DataType{4, 5, 6, 7} + validPaths := []string{ + "/interfaces/interface[name=%s]/config", + "/interfaces/interface[name=%s]/state", + "/interfaces/interface[name=%s]/state/counters", + "/interfaces/interface[name=%s]/state"} + if len(types) != len(validPaths) { + t.Fatalf("types and invalidPaths should be the same size") + } + for i := range validPaths { + path := fmt.Sprintf(validPaths[i], intf) + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + + // Get notifications from a Get request without an explicit type specified + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + notifs, err := notificationsFromGetRequest(t, dut, + &gpb.GetRequest{ + Prefix: prefix, + Path: paths, + // Type: OMITED + Encoding: gpb.Encoding_PROTO, + }) + if err != nil { + t.Fatalf(err.Error()) + } + + // Verify response completeness + i := 0 + for _, notif := range notifs { + pathRootStr, err := ygot.PathToString(&gpb.Path{Elem: notif.GetPrefix().GetElem()}) + if err != nil { + t.Fatalf("failed to convert elems (%v) to string: %v", notif.GetPrefix().GetElem(), err) + } + updates := notif.GetUpdate() + if len(updates) == 0 { + continue + } + for _, update := range updates { + updatePath, err := ygot.PathToString(update.GetPath()) + if err != nil { + t.Fatalf("(%v): failed to convert path (%v) to string (%v): %v", t.Name(), updatePath, prototext.Format(update), err) + } + fullPath := pathRootStr + updatePath + if !strings.HasPrefix(fullPath, path) { + t.Fatalf("(%v): Expected path (%v) to have prefix(%v)", t.Name(), fullPath, path) + } + i++ + } + if i == 0 { + t.Fatalf("(%v): No updates returned for path (%v)", t.Name(), path) + } + } + } +} diff --git a/tests/gnmi_helper.go b/tests/gnmi_helper.go new file mode 100644 index 0000000..6769b8c --- /dev/null +++ b/tests/gnmi_helper.go @@ -0,0 +1,987 @@ +package gnmi_stress_helper + +import ( + "context" + "encoding/json" + "fmt" + "math/rand" + "strconv" + "sync" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ygot/ygot" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/prototext" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/gnmi/value" +) + +// PathInfo structure defines the path info. +type PathInfo struct { + path string + payload string + expectedResult bool + expectedResponse any + isUsingRandomIntf bool +} + +// Paths are used in get and set tests randomly. +var Paths = []PathInfo{ + PathInfo{ + path: "/interfaces/interface[name=%s]/config/mtu", + payload: strconv.FormatUint(uint64(9216), 10), + expectedResult: true, + expectedResponse: uint64(9216), + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/interfaces/interface[name=%s]/config/description", + payload: "\"test\"", + expectedResult: true, + expectedResponse: "\"test\"", + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/interfaces/interface[name=%s]/config/enabled", + payload: strconv.FormatBool(true), + expectedResult: true, + expectedResponse: true, + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/interfaces/interface[name=%s]/config/xyz", + payload: strconv.FormatBool(true), + expectedResult: false, + expectedResponse: `{}`, + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/interfaces/interface[name=%s]/config/description", + payload: "\"This is a description from gnmi helper.\"", + expectedResult: true, + expectedResponse: "\"This is a description from gnmi helper.\"", + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/interfaces/interface[name=%s]/config/health-indicator", + payload: "\"GOOD\"", + expectedResult: true, + expectedResponse: "\"GOOD\"", + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/interfaces/interface[name=%s]/config/fully-qualified-interface-name", + payload: "\"test_interface\"", + expectedResult: false, + expectedResponse: "\"test_interface\"", + isUsingRandomIntf: true, + }, + PathInfo{ + path: "/openconfig-platform:components/abc", + payload: "{name: chassis}", + expectedResult: false, + expectedResponse: `{}`, + isUsingRandomIntf: false, + }, +} + +// Path list is a set of random interface paths. +var Path = []string{ + "/interfaces/interface[name=%s]/config/mtu", + "/interfaces/interface[name=%s]/config/enabled", + "/interfaces/interface[name=%s]/state/type", + "/interfaces/interface[name=%s]/state/cpu", +} + +// DeletePaths is a set of random interface paths for delete operations. +var DeletePaths = []PathInfo{ + PathInfo{ + path: "/interfaces/interface[name=%s]/config/description", + payload: "\"test_interface\"", + }, +} + +// DelSubtree list is the possible combination of gNMI path subtrees. +var DelSubtree = []string{ + "qos/forwarding-groups/", + "qos/queues/", +} + +// Subtree list is the possible combination of gNMI path subtrees. +var Subtree = []string{ + "interfaces/", + "qos/", + "system/", +} + +// list of gNMI operations +var ops = []string{ + "get", + "set", + "subscribe", +} + +// The following payload used as config push payload during set stress tests. +const ( + ShortStressTestInterval = 600000000000 // 10 minute interval in ns + LongStressTestInterval = 28800000000000 // 8 hour interval in ns + IdleTime = 10 // 10 seconds for the DUT to cool down + MinIteration = 6 + AvgIteration = 20 + MinMtuStepInc = 100 + MaxMtuStepInc = 200 + SampleInterval = 2000000000 + Timeout = 3 * time.Second + UpdatesOnly = true +) + +// ConfigPush function to push config via gNMI raw Set. +func ConfigPush(t *testing.T, dut *ondatra.DUTDevice) { + // Create setRequest message. + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: []*gpb.Update{{ + Path: &gpb.Path{Elem: []*gpb.PathElem{{Name: "/"}}}, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("")}}, + }}, + } + + // Fetch set client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Error while calling Set API during config push: (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) +} + +// SanityCheck function validates the sanity of the DUT +func SanityCheck(t *testing.T, dut *ondatra.DUTDevice, ports ...string) { + t.Helper() + if err := testhelper.GNOIAble(t, dut); err != nil { + t.Fatalf("gNOI server is not running in the DUT") + } + if err := testhelper.GNMIAble(t, dut); err != nil { + t.Fatalf("gNMI server is not running in the DUT") + } + if ports != nil { + if err := testhelper.VerifyPortsOperStatus(t, dut, ports...); err != nil { + t.Logf("Ports %v oper status is not up", ports) + t.Fatalf("Ports are not oper upT") + } + } +} + +// CollectPerformanceMetrics collect the system performance metrics via gNMI get +func CollectPerformanceMetrics(t *testing.T, dut *ondatra.DUTDevice) { + t.Helper() + // TODO: Receiving DB connection error for both process and memory path, + // backend is not implemented yet. The following code block can be + // uncommented out once the implementation is complete + /* memory := dut.Telemetry().System().Memory().Get(t) + t.Logf("System memory details:", memory.Physical, memory.Reserved) + + // Create getRequest message with ASCII encoding. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "system", + }, { + Name: "processes", + }}, + }}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Unable to fetch get client (%v)", err) + } + if getResp == nil { + t.Fatalf("Unable to fetch get client, get response is nil") + } + t.Logf("System Processes Info: %v", getResp) + */ +} + +// StressTestHelper function to invoke various gNMI set and get operations +func StressTestHelper(t *testing.T, dut *ondatra.DUTDevice, interval time.Duration) { + SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + CollectPerformanceMetrics(t, dut) + t.Logf("Interval : %v", interval) + + // Simple gNMI get request followed by a gNMI set replace to stress the DUT. + for timeout := time.Now().Add(interval); time.Now().Before(timeout); { + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + pathInfo := Paths[rand.Intn(len(Paths))] + path := pathInfo.path + if pathInfo.isUsingRandomIntf == true { + path = fmt.Sprintf(pathInfo.path, port) + } + t.Logf("path : %v", path) + // Create set the Request. + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + pathList := []*gpb.Path{sPath} + + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Update: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(pathInfo.payload)}}, + }}, + } + t.Logf("SetRequest:\n%v", setRequest) + // Fetch set client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if pathInfo.expectedResult == true && err != nil { + t.Fatalf("Unable to fetch set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: pathList, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + getResp, err := gnmiClient.Get(ctx, getRequest) + if pathInfo.expectedResult == true && err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + + if pathInfo.expectedResult == true && getResp == nil { + t.Fatalf("Get response is nil") + } + t.Logf("GetResponse:\n%v", getResp) + CollectPerformanceMetrics(t, dut) + } + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(IdleTime * time.Second) + CollectPerformanceMetrics(t, dut) + SanityCheck(t, dut) + +} + +// StressSetTestHelper function to invoke various gNMI set and get operations +func StressSetTestHelper(t *testing.T, dut *ondatra.DUTDevice, interval int, replace bool) { + SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + CollectPerformanceMetrics(t, dut) + t.Logf("Interval : %v", interval) + + // Simple gNMI get request followed by a gNMI set replace to stress the DUT. + for i := 0; i < interval; i++ { + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + pathInfo := Paths[rand.Intn(len(Paths))] + path := pathInfo.path + if pathInfo.isUsingRandomIntf == true { + path = fmt.Sprintf(pathInfo.path, port) + } + t.Logf("path : %v", path) + // Create set the Request. + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + getResp := &gpb.GetResponse{} + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: paths, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_JSON_IETF, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if pathInfo.expectedResult == true { + getResp, err = gnmiClient.Get(context.Background(), getRequest) + if err == nil { + t.Logf("The path is not populated") + } + } + + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Update: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(pathInfo.payload)}}, + }}, + } + if replace == true { + setRequest = &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(pathInfo.payload)}}, + }}, + } + } + t.Logf("SetRequest:\n%v", setRequest) + // Fetch set client using the raw gNMI client. + setResp, err := gnmiClient.Set(context.Background(), setRequest) + if pathInfo.expectedResult == true && err != nil { + t.Fatalf("Unable to fetch set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + CollectPerformanceMetrics(t, dut) + + // Restore the old values for the path if the above set resulted in changing the values + if getResp != nil && pathInfo.expectedResult == true { + updates, err := UpdatesWithJSONIETF(getResp) + if err != nil { + t.Fatalf("Unable to get updates with JSON IETF: (%v)", err) + } + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: updates, + } + setResp, err := gnmiClient.Set(context.Background(), setRequest) + if err != nil { + t.Fatalf("Unable to restore the original value using set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + } + + } + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(IdleTime * time.Second) + CollectPerformanceMetrics(t, dut) + SanityCheck(t, dut) + +} + +// ParseGetResponseHelper function will parse the gNMI get response to get the value +func ParseGetResponseHelper(t *testing.T, getResp *gpb.GetResponse) string { + // Validate GET response. + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + notif, updates := notifs[0], notifs[0].GetUpdate() + if len(updates) < 1 { + t.Fatalf("got %d updates in the notification, want 1", len(updates)) + } + pathStr, err := ygot.PathToString(&gpb.Path{Elem: notif.GetPrefix().GetElem()}) + if err != nil { + t.Fatalf("failed to convert elems (%v) to string: %v", notif.GetPrefix().GetElem(), err) + } + + updatePath, err := ygot.PathToString(updates[0].GetPath()) + if err != nil { + t.Fatalf("failed to convert path to string (%v): %v", updatePath, err) + } + gotPath := updatePath + if pathStr != "/" { + gotPath = pathStr + updatePath + } + t.Logf("The path is in the response:%v", gotPath) + val := updates[0].GetVal() + + var gotVal any + if val.GetJsonIetfVal() == nil { + // Get Scalar value. + gotVal, err = value.ToScalar(val) + if err != nil { + t.Logf("got %v, want scalar value", gotVal) + } + gotVal = fmt.Sprintf("%v", gotVal) + } else { + // Unmarshal json data to container. + if err := json.Unmarshal(val.GetJsonIetfVal(), &gotVal); err != nil { + t.Logf("could not unmarshal json data to container: %v", err) + } + } + return gotVal.(string) +} + +// SetDifferentClientTest function to invoke set operation from different clients +func SetDifferentClientTest(t *testing.T, dut *ondatra.DUTDevice, replace bool) { + SanityCheck(t, dut) + ctx := context.Background() + newGNMIClient := func() gpb.GNMIClient { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + return gnmiClient + } + clients := map[string]gpb.GNMIClient{ + "c1": newGNMIClient(), + "c2": newGNMIClient(), + "c3": newGNMIClient(), + "c4": newGNMIClient(), + "c5": newGNMIClient(), + "c6": newGNMIClient(), + "c7": newGNMIClient(), + "c8": newGNMIClient(), + "c9": newGNMIClient(), + "c10": newGNMIClient(), + } + + var wg sync.WaitGroup + for k := range clients { + v := k + wg.Add(1) + rand.Seed(time.Now().Unix()) + CollectPerformanceMetrics(t, dut) + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + // Create Set Request. + pathInfo := Paths[rand.Intn(len(Paths))] + path := pathInfo.path + if pathInfo.isUsingRandomIntf == true { + path = fmt.Sprintf(pathInfo.path, port) + } + t.Logf("path : %v", path) + // Create set the Request. + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Update: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(pathInfo.payload)}}, + }}, + } + if replace == true { + setRequest = &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(pathInfo.payload)}}, + }}, + } + } + t.Logf("SetRequest:\n%v", setRequest) + + // Fetch get client using the raw gNMI client. + go func() { + setResp, err := clients[v].Set(context.Background(), setRequest) + if err != nil { + t.Log("Error while calling Get Raw API") + } + if setResp == nil { + t.Log("Get response is nil") + } + t.Logf("GetResponse:\n%v", setResp) + wg.Done() + }() + + CollectPerformanceMetrics(t, dut) + } + wg.Wait() + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(IdleTime * time.Second) + CollectPerformanceMetrics(t, dut) + SanityCheck(t, dut) +} + +// SetDefaultValuesHelper function will set the default values for the paths if it doesn't exist already. +func SetDefaultValuesHelper(t *testing.T, dut *ondatra.DUTDevice, port string) { + for i := 0; i < len(DeletePaths); i++ { + pathInfo := DeletePaths[i] + reqPath := fmt.Sprintf(pathInfo.path, port) + // Create Set Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + // Set the default value for the path if there is none + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Update: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(pathInfo.payload)}}, + }}, + } + t.Logf("SetRequest:\n%v", setRequest) + // Fetch set client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to fetch set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + } +} + +// RandomDeletePath function will choose a ramdom path from list of paths +func RandomDeletePath(port string) string { + pathInfo := DeletePaths[rand.Intn(len(DeletePaths))] + reqPath := fmt.Sprintf(pathInfo.path, port) + return reqPath +} + +// VerifyFullResponse function will verify the subscription response message +func VerifyFullResponse(t *testing.T, subClient gpb.GNMI_SubscribeClient, timeout time.Duration) { + t.Helper() + // Process response from DUT. + for { + // Wait for response from DUT. + res, err := subClient.Recv() + if err != nil { + t.Fatalf("Response error received from DUT (%v)", err) + } + + switch v := res.Response.(type) { + case *gpb.SubscribeResponse_Update: + // Process Update message received in SubscribeResponse. + updates := v.Update + // Perform basic sanity on the Update message. + for _, update := range updates.GetUpdate() { + if update.Path == nil { + t.Errorf("Invalid nil Path in update: %v", prototext.Format(update)) + continue + } + if update.Val == nil { + t.Errorf("Invalid nil Val in update: %v", prototext.Format(update)) + continue + } + + // Path is partially present in Prefix and partially in Update in the response. + prefixStr, err := ygot.PathToString(updates.GetPrefix()) + if err != nil { + t.Errorf("Failed to convert path to string (%v) %v", err, updates.GetPrefix()) + continue + } + elemStr, err := ygot.PathToString(update.Path) + if err != nil { + t.Errorf("Failed to convert path to string (%v) %v", err, update.Path) + continue + } + pathStr := prefixStr + elemStr + t.Logf("Path in the response:%v", pathStr) + } + return + } + } +} + +// StressTestSubsHelper function to invoke various subscription operations +func StressTestSubsHelper(t *testing.T, dut *ondatra.DUTDevice, subtree bool, poll bool) { + SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + CollectPerformanceMetrics(t, dut) + for i := 0; i < MinIteration; i++ { + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + reqPath := fmt.Sprintf(Path[rand.Intn(len(Path))], port) + if subtree == true { + reqPath = fmt.Sprintf(Subtree[rand.Intn(len(Subtree))]) + } + // Create Subscribe Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + req := &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: sPath.Elem}, + Mode: gpb.SubscriptionMode_SAMPLE, + SampleInterval: SampleInterval, + }}, + Mode: gpb.SubscriptionList_STREAM, + Encoding: gpb.Encoding_PROTO, + UpdatesOnly: UpdatesOnly, + }, + }, + } + if poll == true { + req = &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: sPath.Elem}, + }}, + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Mode: gpb.SubscriptionList_POLL, + Encoding: gpb.Encoding_PROTO, + }, + }, + } + } + t.Logf("Subscribe request:%v", req) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(req); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + if _, err := subscribeClient.Recv(); err != nil { + t.Fatalf("Failed to receive gNMI sample subscribe request (%v)", err) + } + CollectPerformanceMetrics(t, dut) + } + t.Logf("After %v seconds of idle time, the performance metrics are collected", IdleTime) + time.Sleep(IdleTime * time.Second) + CollectPerformanceMetrics(t, dut) + SanityCheck(t, dut) +} + +// SubscribeDifferentClientTest function to invoke set operation from different clients +func SubscribeDifferentClientTest(t *testing.T, dut *ondatra.DUTDevice, poll bool) { + SanityCheck(t, dut) + clients := map[string]gpb.GNMIClient{} + ctx := context.Background() + for i := 1; i <= 10; i++ { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + clients[fmt.Sprintf("c%d", i)] = gnmiClient + } + + var wg sync.WaitGroup + for k := range clients { + v := k + wg.Add(1) + SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + CollectPerformanceMetrics(t, dut) + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + reqPath := fmt.Sprintf(Path[rand.Intn(len(Path))], port) + // Create Subscribe Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + req := &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: sPath.Elem}, + Mode: gpb.SubscriptionMode_SAMPLE, + SampleInterval: SampleInterval, + }}, + Mode: gpb.SubscriptionList_STREAM, + Encoding: gpb.Encoding_PROTO, + UpdatesOnly: UpdatesOnly, + }, + }, + } + if poll == true { + req = &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: sPath.Elem}, + }}, + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Mode: gpb.SubscriptionList_POLL, + Encoding: gpb.Encoding_PROTO, + }, + }, + } + } + t.Logf("Subscribe request:%v", req) + // Fetch get client using the raw gNMI client. + go func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + client, err := clients[v].Subscribe(ctx) + if err != nil { + t.Logf("Unable to get subscribe client (%v)", err) + } + if err := client.Send(req); err != nil { + t.Logf("Failed to send gNMI subscribe request (%v)", err) + } + _, error := client.Recv() + if error != nil { + t.Logf("Response error received from DUT (%v)", error) + } + CollectPerformanceMetrics(t, dut) + wg.Done() + }() + } + wg.Wait() + t.Logf("After %v seconds of idle time, the performance metrics are collected", IdleTime) + time.Sleep(IdleTime * time.Second) + CollectPerformanceMetrics(t, dut) + SanityCheck(t, dut) +} + +func selectgNMIPathHelper(t *testing.T, ops string, port string) (string, string, bool) { + t.Helper() + var path string + if ops == "set" { + pathInfo := Paths[rand.Intn(len(Paths))] + path = pathInfo.path + if pathInfo.isUsingRandomIntf == true { + path = fmt.Sprintf(pathInfo.path, port) + } + return path, pathInfo.payload, pathInfo.expectedResult + } + path = fmt.Sprintf(Path[rand.Intn(len(Path))], port) + return path, "", false +} + +// RandomDifferentClientTestHelper function to invoke various gNMI operation from different clients +func RandomDifferentClientTestHelper(t *testing.T, dut *ondatra.DUTDevice, interval time.Duration) { + SanityCheck(t, dut) + clients := map[string]gpb.GNMIClient{} + ctx := context.Background() + newGNMIClient := func() gpb.GNMIClient { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + return gnmiClient + } + for i := 1; i <= MinIteration; i++ { + clients[fmt.Sprintf("c%d", i)] = newGNMIClient() + } + + var wg sync.WaitGroup + t.Logf("Interval : %v", interval) + for timeout := time.Now().Add(interval); time.Now().Before(timeout); { + // Create a gNMI Request. + rand.Seed(time.Now().Unix()) + ops := ops[rand.Intn(len(ops))] + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + path, payload, expectedResult := selectgNMIPathHelper(t, ops, port) + t.Logf("The path expect the valid result: %v", expectedResult) + sPath, err := ygot.StringToStructuredPath(path) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + for k := range clients { + v := k + wg.Add(1) + CollectPerformanceMetrics(t, dut) + if ops == "get" { + // Create getRequest message with data type. + req := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{sPath}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", req) + // Fetch get client using the raw gNMI client. + go func() { + setResp, err := clients[v].Get(ctx, req) + if err != nil { + t.Log("Error while calling Get Raw API") + } + if setResp == nil { + t.Log("Get response is nil") + } + t.Logf("GetResponse:\n%v", setResp) + wg.Done() + }() + } else if ops == "set" { + paths := []*gpb.Path{sPath} + getResp := &gpb.GetResponse{} + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: paths, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_JSON_IETF, + } + t.Logf("GetRequest:\n%v", getRequest) + + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + + // Fetch get client using the raw gNMI client. + if expectedResult == true { + getResp, err = gnmiClient.Get(ctx, getRequest) + if err == nil { + t.Logf("The path is not populated") + } + } + req := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Update: []*gpb.Update{{ + Path: sPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte(payload)}}, + }}, + } + t.Logf("SetRequest:\n%v", req) + // Fetch set client using the raw gNMI client. + go func() { + setResp, err := clients[v].Set(ctx, req) + if err != nil { + t.Logf("Error while calling Set Raw API: %v\n", err) + } + if setResp == nil { + t.Log("Set response is nil") + } + t.Logf("SetResponse:\n%v", setResp) + wg.Done() + }() + // Restore the old values for the path if the above set resulted in changing the values + if getResp != nil && expectedResult == true { + updates, err := UpdatesWithJSONIETF(getResp) + if err != nil { + t.Fatalf("Unable to get updates with JSON IETF: (%v)", err) + } + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: updates, + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to restore the original value using set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + } + } else { + req := &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: sPath.Elem}, + }}, + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Mode: gpb.SubscriptionList_ONCE, + Encoding: gpb.Encoding_PROTO, + }, + }, + } + t.Logf("Subscribe request:%v", req) + // Fetch get client using the raw gNMI client. + go func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + client, err := clients[v].Subscribe(ctx) + if err != nil { + t.Logf("Unable to get subscribe client (%v)", err) + } + if err := client.Send(req); err != nil { + t.Logf("Failed to send gNMI subscribe request (%v)", err) + } + _, error := client.Recv() + if error != nil { + t.Logf("Response error received from DUT (%v)", error) + } + wg.Done() + }() + } + CollectPerformanceMetrics(t, dut) + } + wg.Wait() + } + t.Logf("After %v seconds of idle time, the performance metrics are collected", IdleTime) + time.Sleep(IdleTime * time.Second) + CollectPerformanceMetrics(t, dut) + SanityCheck(t, dut) +} + +// UpdatesWithJSONIETF parses a Get Response and returns the Updates in the correct format +// to be used in a Set Request. This is useful for restoring the contents of a Get Response. The +// Get Response must be encoded in JSON IETF, specified by the Get Request. +func UpdatesWithJSONIETF(getResp *gpb.GetResponse) ([]*gpb.Update, error) { + updates := []*gpb.Update{} + for _, notification := range getResp.GetNotification() { + if notification == nil { + return nil, fmt.Errorf("Notification in GetResponse is empty") + } + for _, update := range notification.GetUpdate() { + if update == nil { + return nil, fmt.Errorf("Update in Notification is empty") + } + jsonVal := update.GetVal().GetJsonIetfVal() + jsonMap := make(map[string]json.RawMessage) + err := json.Unmarshal(jsonVal, &jsonMap) + if err != nil { + return nil, err + } + if len(jsonMap) == 1 { + for _, v := range jsonMap { + jsonVal, err = v.MarshalJSON() + if err != nil { + return nil, err + } + } + } + updates = append(updates, &gpb.Update{ + Path: update.GetPath(), + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: jsonVal}}, + }) + } + } + return updates, nil +} diff --git a/tests/gnmi_long_stress_test.go b/tests/gnmi_long_stress_test.go new file mode 100644 index 0000000..3140a7d --- /dev/null +++ b/tests/gnmi_long_stress_test.go @@ -0,0 +1,21 @@ +package gnmi_long_stress_test + +import ( + "testing" + + "github.com/openconfig/ondatra" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + gst "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/tests/gnmi_stress_helper" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// gNMI long stress test (8 hour) +func TestGNMILongStressTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("37aea757-8eb4-41a8-87b7-aa26013cfe47").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressTestHelper(t, dut, gst.LongStressTestInterval) +} diff --git a/tests/gnmi_set_get_test.go b/tests/gnmi_set_get_test.go new file mode 100644 index 0000000..ad35fd3 --- /dev/null +++ b/tests/gnmi_set_get_test.go @@ -0,0 +1,1769 @@ +package gnmi_set_get_test + +import ( + "context" + "fmt" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/testt" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/openconfig/ygot/ygot" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/prototext" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +) + +const ( + testdataDir = "./" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +/********************************************************** +* gNMI SET Update operations +**********************************************************/ +func TestGNMISetUpdateSingleLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("ffe66b7b-0e61-49bd-803d-0406a8c914d7").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + mtu := gnmi.Get(t, dut, mtuPath.Config()) + // Adding 22 bytes to the existing MTU value for the test. + newMtu := mtu + 22 + + defer func() { + // Replace the old value for the MTU field as a test cleanup. + gnmi.Replace(t, dut, mtuPath.Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, mtu) + }() + + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Update: []*gpb.Update{{ + Path: resolvedPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(newMtu), 10)), + }, + }, + }}, + } + + enabled := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().Config()) + ctx := context.Background() + + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to fetch set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + + // Verify the value is being set properly using get. + if got := gnmi.Get(t, dut, mtuPath.Config()); got != newMtu { + t.Errorf("MTU matched failed! got:%v, want:%v", got, newMtu) + } + + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().Config()); got != enabled { + t.Errorf("Enabled matched failed! got:%v, want:%v", got, enabled) + } +} + +func TestGNMISetUpdateNonExistingLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("408e875e-d00f-4071-acaf-204616800bee").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + descPath := gnmi.OC().Interface(intf).Description() + resolvedPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, errs) + } + ctx := context.Background() + var desc string = "description before reset" + var descExist bool = false + if res.Description != nil { + desc = *res.Description + descExist = true + var paths []*gpb.Path + paths = append(paths, resolvedPath) + + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: paths, + } + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + delResp, err := gnmiClient.Set(ctx, delRequest) + if err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + t.Logf("SetResponse:\n%v", delResp) + } + + wantDesc := "description after reset" + defer func() { + // Replace the id to original value if it exists before the test. + if descExist { + gnmi.Delete(t, dut, descPath.Config()) + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, desc) + } + }() + + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Update: []*gpb.Update{{ + Path: resolvedPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(fmt.Sprintf("\"%s\"", wantDesc)), + }, + }, + }}, + } + + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(context.Background(), grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to fetch set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + // Verify the value is being set properly using get. + if got := gnmi.Get(t, dut, descPath.Config()); got != wantDesc { + t.Errorf("ID matched failed! got:%v, want: description after reset", got) + } + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU matched failed! mtuAfterSet:%v, want:%v", got, mtu) + } +} + +func TestGNMISetUpdateMultipleLeafs(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("fc046164-bd3f-44f5-8056-7ff8df404909").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Get fields from interface subtree. + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + descPath := gnmi.OC().Interface(intf).Description() + resolvedDescPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, err) + } + var desc string + desExist := false + + if res.Description != nil { + desc = *res.Description + desExist = true + } + + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedMtuPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + mtu := res.GetMtu() + // Adding 22 bytes to the existing MTU value for the test. + wantMtu := mtu + 22 + wantDesc := "This is a wanted description." + enabled := res.GetEnabled() + id := res.GetId() + fqin := testhelper.FullyQualifiedInterfaceName(t, dut, intf) + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + defer func() { + // Replace the old values for test cleanup. + gnmi.Replace(t, dut, mtuPath.Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, mtu) + if desExist { + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, desc) + } else { + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedDescPath}, + } + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Set(ctx, delRequest); err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + } + }() + + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Update: []*gpb.Update{ + { + Path: resolvedMtuPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(wantMtu), 10)), + }, + }, + }, + { + Path: resolvedDescPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(fmt.Sprintf("\"%s\"", wantDesc)), + }, + }, + }, + }, + } + + // Fetch raw gNMI client and call Set API to send Set Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Error while calling Set Raw API: (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + + // Verify the values are set properly using get. + // Get fields from interface subtree. + intfAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + if got := intfAfterSet.GetMtu(); got != wantMtu { + t.Errorf("MTU match failed! got: %v, want: %v", got, wantMtu) + } + if got := intfAfterSet.GetDescription(); got != wantDesc { + t.Errorf("Description match failed! got: %v, want: %v", got, wantDesc) + } + + // Verify that other leaf nodes are not changed. + if got := intfAfterSet.GetEnabled(); got != enabled { + t.Errorf("Enabled match failed! got %v, want %v", got, enabled) + } + if got := intfAfterSet.GetId(); got != id { + t.Errorf("ID match failed! got %v, want %v", got, id) + } + if got := testhelper.FullyQualifiedInterfaceName(t, dut, intf); got != fqin { + t.Errorf("FullyQualifiedInterfaceName match failed! got %v, want %v", got, fqin) + } +} + +func TestGNMISetUpdateInvalidDataLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("6dc4b8de-f5d8-406d-b9c1-9530eecefd3b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + path := &gpb.Path{Elem: []*gpb.PathElem{{Name: "platform"}}} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Update: []*gpb.Update{{ + Path: path, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("{\"openconfig-interfaces:description:\":\"test\"}"), + }, + }, + }}, + } + + // Fetch raw gNMI client and call Set API to send Set Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Set(ctx, setRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } +} + +func TestGNMISetUpdateInvalidLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("6dcd41e7-a491-4d71-a52a-0ea4f446400a").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + path := &gpb.Path{Elem: []*gpb.PathElem{{Name: "interfaces"}, {Name: "interface", Key: map[string]string{"name": intf}}, {Name: "config"}, {Name: "xyz"}}} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Update: []*gpb.Update{{ + Path: path, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("123"), + }, + }, + }}, + } + + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + ctx := context.Background() + + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err = gnmiClient.Set(ctx, setRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } + + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU matched failed! mtuAfterSet:%v, want:%v", got, mtu) + } + +} + +/********************************************************** +* gNMI SET Replace operations +**********************************************************/ +// Sample test that performs gNMI SET replace on leaf. +func TestGNMISetReplaceSingleLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("162ec144-03b2-45ba-8bab-975ae4d09f7a").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + enabled := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().Config()) + oldMtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + defer func() { + // Replace the old MTU value as a test cleanup. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), oldMtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, oldMtu) + }() + + // Configure port MTU and verify that state path reflects configured MTU. + mtu := uint16(1500) + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), mtu) + + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU matched failed! got:%v, want:%v", got, mtu) + } + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().Config()); got != enabled { + t.Errorf("enabled matched failed! idAfterSet:%v, want:%v", got, enabled) + } + +} + +func TestGNMISetReplaceMoreThanOneLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("d89eb043-3594-4265-bf91-5e71477f98b9").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + oldEnabled := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().Config()) + oldMtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + intfType := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Type().Config()) + mtu := oldMtu + 22 + enabled := strconv.FormatBool(!oldEnabled) + + defer func() { + // Replace the old values as a test cleanup. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), oldEnabled) + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), oldMtu) + // Wait for the port to be operationally up. + gnmi.Await(t, dut.GNMIOpts().WithYGNMIOpts(ygnmi.WithSubscriptionMode(gpb.SubscriptionMode_ON_CHANGE)), gnmi.OC().Interface(intf).OperStatus().State(), 30*time.Second, oc.Interface_OperStatus_UP) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Enabled().State(), 1*time.Second, oldEnabled) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, oldMtu) + }() + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + path := &gpb.Path{Elem: []*gpb.PathElem{{Name: "interfaces"}, {Name: "interface", Key: map[string]string{"name": intf}}, {Name: "openconfig-interfaces:config"}}} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{{ + Path: path, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("{\"enabled\":" + enabled + ",\"mtu\":" + strconv.FormatUint(uint64(mtu), 10) + "}"), + }, + }, + }}, + } + ctx := context.Background() + + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to fetch set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + + // Verify the value is being set properly using get. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU matched failed! got:%v, want:%v", got, mtu) + } + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().Config()); got == oldEnabled { + t.Errorf("Enabled matched failed! got:%v, want:%v", got, !oldEnabled) + } + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Type().Config()); got != intfType { + t.Errorf("Type matched failed! got type :%v, want:%v", got, intfType) + } +} + +func TestGNMISetReplaceInvalidDataLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("31509436-ddc5-405b-9008-0b71d28fbb92").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + path := &gpb.Path{Elem: []*gpb.PathElem{{Name: "platform"}}} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{{ + Path: path, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("{\"openconfig-interfaces:description:\":\"test\"}"), + }, + }, + }}, + } + ctx := context.Background() + + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } + t.Logf("SetResponse:\n%v", setResp) +} + +func TestGNMISetReplaceInvalidLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("a9e707cf-4872-4066-ab38-8fe2c14af892").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + path := &gpb.Path{Elem: []*gpb.PathElem{{Name: "interfaces"}, {Name: "interface", Key: map[string]string{"name": intf}}, {Name: "config"}, {Name: "xyz"}}} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{{ + Path: path, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("123"), + }, + }, + }}, + } + + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + ctx := context.Background() + + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err = gnmiClient.Set(ctx, setRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } + + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU matched failed! got:%v, want:%v", got, mtu) + } + +} + +func TestGNMISetReplaceMultipleLeafsValid(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c1f9dd81-d509-405f-bed2-e108e619b5f6").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Get fields from interface subtree. + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + descPath := gnmi.OC().Interface(intf).Description() + resolvedDescPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, err) + } + var desc, wantDesc string + descExist := false + if res.Description != nil { + desc = *res.Description + descExist = true + } + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedMtuPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + mtu := res.GetMtu() + // Adding 22 bytes to the existing MTU value for the test. + wantMtu := mtu + 22 + wantDesc = "wanted description" + enabled := res.GetEnabled() + fqin := testhelper.FullyQualifiedInterfaceName(t, dut, intf) + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + defer func() { + // Replace the old values for test cleanup. + gnmi.Replace(t, dut, mtuPath.Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, mtu) + if descExist { + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 20*time.Second, desc) + } else { + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedDescPath}, + } + // Fetch set client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Set(ctx, delRequest); err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + } + }() + + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{ + { + Path: resolvedMtuPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(wantMtu), 10)), + }, + }, + }, + { + Path: resolvedDescPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(fmt.Sprintf("\"%s\"", wantDesc)), + }, + }, + }, + }, + } + + // Fetch raw gNMI client and call Set API to send Set Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Error while calling Set Raw API: (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + + // Get fields from interface subtree. + intfAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + // Verify the values are set properly using get. + if got := intfAfterSet.GetMtu(); got != wantMtu { + t.Errorf("MTU match failed! got: %v, want: %v", got, wantMtu) + } + if got := intfAfterSet.GetDescription(); got != wantDesc { + t.Errorf("Description match failed! got %v, want %v", got, wantDesc) + } + + // Verify that other leaf nodes are not changed. + if got := intfAfterSet.GetEnabled(); got != enabled { + t.Errorf("Enabled match failed! got %v, want %v", got, enabled) + } + + if got := testhelper.FullyQualifiedInterfaceName(t, dut, intf); got != fqin { + t.Errorf("FullyQualifiedInterfaceName match failed! got %v, want %v", got, fqin) + } +} + +func TestGNMISetReplaceMultipleLeafsInvalid(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c485b29b-7e1e-4b5a-bccd-797ced277008").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Get fields from interface subtree. + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + mtu := gnmi.Get(t, dut, mtuPath.Config()) + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{ + { + Path: resolvedPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(mtu+22), 10)), + }, + }, + }, + { + Path: &gpb.Path{ + Elem: []*gpb.PathElem{ + { + Name: "interfaces", + }, + { + Name: "interface", + Key: map[string]string{"name": intf}, + }, + { + Name: "config", + }, + { + Name: "openconfig-abc:xyz", + }, + }, + }, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("987"), + }, + }, + }, + }, + } + + // Fetch raw gNMI client and call Set API to send Set Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err = gnmiClient.Set(ctx, setRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } + + // Verify that the MTU value did not get changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU match failed! gotMtu %v, want %v", got, mtu) + } +} + +/********************************************************** +* gNMI SET Delete operations +**********************************************************/ +func TestGNMISetDeleteSingleLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("01850837-93b3-44a6-9d8f-84a0bd6c8725").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + desPath := gnmi.OC().Interface(intf).Description() + resolvedPath, _, errs := ygnmi.ResolvePath(desPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", desPath, err) + } + ctx := context.Background() + var des string = "" + var desExist bool = false + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + if res.Description != nil { + des = *res.Description + desExist = true + } else { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Description().Config(), "Test description from ondatra test") + } + paths := []*gpb.Path{resolvedPath} + + defer func() { + // Replace the id to original value if it exists before the test. + if desExist { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Description().Config(), des) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, des) + } + }() + + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: paths, + } + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + delResp, err := gnmiClient.Set(ctx, delRequest) + if err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + t.Logf("SetResponse:\n%v", delResp) + + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Errorf("MTU matched failed! got:%v, want:%v", got, mtu) + } +} + +func TestGNMISetDeleteMultipleLeafs(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("66addddd-df5d-4ff0-91ca-ff2a3582be69").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + ctx := context.Background() + + descPath := gnmi.OC().Interface(intf).Description() + resolvedDescPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, err) + } + var desc string + descExist := false + if res.Description != nil { + desc = *res.Description + descExist = true + } else { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Description().Config(), "desc") + } + + defaultMtu := uint16(9100) + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedMtuPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + + if res.Mtu == nil { + t.Fatalf("MTU should not be nil!") + } + + mtu := uint16(2123) + nonDefaultMtu := false // Default value of MTU is 9100 + if *res.Mtu != defaultMtu { + mtu = *res.Mtu + nonDefaultMtu = true + } else { + gnmi.Update(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 20*time.Second, mtu) + } + + defer func() { + // Replace the fields to original values if they existed before the test. + if descExist { + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, desc) + } + if nonDefaultMtu { + gnmi.Replace(t, dut, mtuPath.Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, mtu) + } + }() + + paths := []*gpb.Path{resolvedDescPath, resolvedMtuPath} + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: paths, + } + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + delResp, err := gnmiClient.Set(ctx, delRequest) + if err != nil { + t.Fatalf("Error while calling Set Delete Raw API(%v)", err) + } + t.Logf("SetResponse:\n%v", delResp) + + // Verify desc leaf is deleted. + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Get(t, dut, descPath.Config()) + }) + // Verify MTU leaf is deleted (set to default value) + if gotMtu := gnmi.Get(t, dut, mtuPath.Config()); gotMtu != defaultMtu { + t.Fatalf("Default MTU matched failed! got:%v, want:%v", gotMtu, defaultMtu) + } +} + +func TestGNMISetDeleteInvalidLeaf(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("bc463964-5e63-417f-bd32-08f17faf84a3").Teardown(t) + dut := ondatra.DUT(t, "DUT") + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + path := &gpb.Path{Elem: []*gpb.PathElem{{Name: "interfaces"}, {Name: "interface", Key: map[string]string{"name": intf}}, {Name: "config"}, {Name: "xyz"}}} + ctx := context.Background() + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + + paths := []*gpb.Path{path} + + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: paths, + } + // Fetch raw gNMI client and call Set API to send Set Request. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + delResp, err := gnmiClient.Set(ctx, delRequest) + if err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } + t.Logf("SetResponse:\n%v", delResp) + + // Verify that other leaf nodes are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()); got != mtu { + t.Fatalf("MTU matched failed! got:%v, want:%v", got, mtu) + } +} + +/********************************************************** +* gNMI SET misc operations +**********************************************************/ + +/* Test to verify the order of SET operations in the same request. + * Verify that specified leaf has been deleted, and other specified leafs + * have been replaced, followed with updating of leaf values. + */ +func TestGNMISetDeleteReplaceUpdateOrderValid(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("fd8b8e2c-69dc-406c-99fd-6bdcf670e17d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Get fields from interface subtree. + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedMtuPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + + mtuExist := false + mtu := uint16(9191) + if res.Mtu != nil { + mtu = *res.Mtu + mtuExist = true + } else { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), mtu) + } + + descPath := gnmi.OC().Interface(intf).Description() + resolvedDescPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, err) + } + desc := "desc" + descExist := false + if res.Description != nil { + desc = *res.Description + descExist = true + } else { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Description().Config(), desc) + } + + fqinPath := fmt.Sprintf("/interfaces/interface[name=%s]/config/fully-qualified-interface-name", intf) + resolvedFqinPath, errs := testhelper.StringToYgnmiPath(fqinPath) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", fqinPath, err) + } + fqin := "FQIN" + fqinExist := false + if name := testhelper.FullyQualifiedInterfaceName(t, dut, intf); name != "" { + fqin = name + fqinExist = true + } else { + testhelper.ReplaceFullyQualifiedInterfaceName(t, dut, intf, fqin) + } + wantFqin := "testFQIN" + wantMtu := mtu + 22 + + enabled := res.GetEnabled() + loopBack := res.GetLoopbackMode() + name := res.GetName() + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + defer func() { + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + + // Replace the old values for test cleanup. + if mtuExist { + gnmi.Replace(t, dut, mtuPath.Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, mtu) + } else { + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedMtuPath}, + } + // Fetch set client using the raw gNMI client. + if _, err := gnmiClient.Set(ctx, delRequest); err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + } + if descExist { + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, desc) + } else { + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedDescPath}, + } + // Fetch set client using the raw gNMI client. + if _, err := gnmiClient.Set(ctx, delRequest); err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + } + if fqinExist { + testhelper.ReplaceFullyQualifiedInterfaceName(t, dut, intf, fqin) + } else { + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedFqinPath}, + } + // Fetch set client using the raw gNMI client. + if _, err := gnmiClient.Set(ctx, delRequest); err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + } + }() + + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedMtuPath, resolvedDescPath}, + Replace: []*gpb.Update{ + { + Path: resolvedMtuPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(wantMtu), 10)), + }, + }, + }, + { + Path: resolvedFqinPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte("\"tempFQIN\""), + }, + }, + }, + }, + Update: []*gpb.Update{{ + Path: resolvedFqinPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("\"" + wantFqin + "\"")}}, + }}, + } + + // Fetch raw gNMI client and call Set API to send Set Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Set(ctx, setRequest); err != nil { + t.Fatalf("Error while calling Set Raw API: (%v)", err) + } + + // Verify that the Description leaf is deleted. + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Get(t, dut, descPath.Config()) + }) + // Get fields from interface subtree. + intfAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).State()) + if got := testhelper.FullyQualifiedInterfaceName(t, dut, intf); got != wantFqin { + t.Errorf("FullyQualifiedInterfaceName match failed! got %v, want %v", got, wantFqin) + } + if got := intfAfterSet.GetMtu(); got != wantMtu { + t.Errorf("mtu match failed! got: %v, want: %v", got, wantMtu) + } + + // Verify that other leaf nodes are not changed. + if got := intfAfterSet.GetEnabled(); got != enabled { + t.Errorf("enabled match failed! got %v, want %v", got, enabled) + } + if got := intfAfterSet.GetLoopbackMode(); got != loopBack { + t.Errorf("loopback-mode match failed! got: %v, want: %v", got, loopBack) + } + if got := intfAfterSet.GetName(); got != name { + t.Errorf("name match failed! got: %v, want: %v", got, name) + } +} + +/* Test to verify the order of SET operations in the same request. + * Verify that the specified path has been deleted and other + * specified path attributes have been updated. */ +func TestGNMISetDeleteUpdateOrderValid(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b7f99b79-f4fb-4c56-95be-602ad0361ec0").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Get fields from interface subtree. + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + descPath := gnmi.OC().Interface(intf).Description() + resolvedDescPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, err) + } + desc := "desc" + descExist := false + if res.Description != nil { + desc = *res.Description + descExist = true + } else { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Description().Config(), desc) + } + wantDesc := "testDescription" + + enabled := res.GetEnabled() + mtu := res.GetMtu() + + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + defer func() { + // Replace the old values for test cleanup. + if descExist { + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, desc) + } else { + delRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedDescPath}, + } + // Fetch set client using the raw gNMI client. + if _, err := gnmiClient.Set(ctx, delRequest); err != nil { + t.Fatalf("Unable to fetch set delete client (%v)", err) + } + } + }() + + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedDescPath}, + Update: []*gpb.Update{{ + Path: resolvedDescPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("\"" + wantDesc + "\"")}}, + }}, + } + + // Fetch raw gNMI client and call Set API to send Set Request. + if _, err := gnmiClient.Set(ctx, setRequest); err != nil { + t.Fatalf("Error while calling Set Raw API: (%v)", err) + } + + // Get fields from interface subtree. + intfAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + if got := intfAfterSet.GetDescription(); got != wantDesc { + t.Errorf("Description match failed! got %v, want %v", got, wantDesc) + } + + // Verify that other leaf nodes are not changed. + if got := intfAfterSet.GetEnabled(); got != enabled { + t.Errorf("Enabled match failed! got %v, want %v", got, enabled) + } + if got := intfAfterSet.GetMtu(); got != mtu { + t.Errorf("MTU match failed! got: %v, want: %v", got, mtu) + } +} + +/* Test to verify the order of SET operations in the same request. + * Verify that with delete followed by update in same SET Request, + * an error message related to the invalid update is returned. */ +func TestGNMISetDeleteUpdateOrderInvalid(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b43ed0ae-22c2-4c25-b50a-96c7243255d9").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Get fields from interface subtree. + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + descPath := gnmi.OC().Interface(intf).Description() + resolvedDescPath, _, errs := ygnmi.ResolvePath(descPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", descPath, err) + } + desc := "desc" + descExist := false + if res.Description != nil { + desc = *res.Description + descExist = true + } + + enabled := res.GetEnabled() + mtu := res.GetMtu() + + defer func() { + // Replace the old values for test cleanup. + if descExist { + gnmi.Replace(t, dut, descPath.Config(), desc) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Description().State(), 5*time.Second, desc) + } + }() + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + setRequest := &gpb.SetRequest{ + Prefix: prefix, + Delete: []*gpb.Path{resolvedDescPath}, + Update: []*gpb.Update{{ + Path: resolvedDescPath, + Val: &gpb.TypedValue{Value: &gpb.TypedValue_JsonIetfVal{JsonIetfVal: []byte("123")}}, + }}, + } + + // Verify that error message is returned for invalid update request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Set(ctx, setRequest); err == nil { + t.Fatalf("Error expected while calling Set Raw API") + } + + // Get fields from interface subtree. + intfAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + + // Verify that other leaf nodes are not changed. + if got := intfAfterSet.GetEnabled(); got != enabled { + t.Errorf("Enabled match failed! got %v, want %v", got, enabled) + } + if got := intfAfterSet.GetMtu(); got != mtu { + t.Errorf("MTU match failed! got: %v, want: %v", got, mtu) + } +} + +/* Test that performs gNMI SET for an empty path. Verify that there are no errors + * returned by the server and a valid response has been sent to the client, + * also verifies that none of the paths are changed. */ +func TestGNMISetEmptyPath(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("0b2e92b0-1295-4aa7-a27d-99073c09e2b7").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + // Get fields from interface subtree. + intfBeforeSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + + // Create setRequest message with an empty path. + setRequest := &gpb.SetRequest{ + Prefix: prefix, + } + + // Fetch set client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Set(ctx, setRequest); err != nil { + t.Fatalf("Error while calling Set API with empty update: (%v)", err) + } + + // Verify that the leaf values did not change. + intfAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + cmpOptions := cmp.Options{cmpopts.IgnoreFields(oc.Interface_Ethernet{}, "Counters"), + cmpopts.IgnoreFields(oc.Interface{}, "Counters")} + if diff := cmp.Diff(intfBeforeSet, intfAfterSet, cmpOptions); diff != "" { + t.Fatalf("diff (-want +got): %v", diff) + } +} + +/* Test that performs gNMI SET with two gNMI clients. */ +func TestGNMIMultipleClientSet(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("753e4dfc-5dda-4bfe-8b1c-e377e6c458ad").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + mtuPath := gnmi.OC().Interface(intf).Mtu() + resolvedMtuPath, _, errs := ygnmi.ResolvePath(mtuPath.Config().PathStruct()) + if errs != nil { + t.Fatalf("Failed to resolve path %v: %v", mtuPath, err) + } + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().Config()) + defer func() { + // Replace the old value for the MTU field as a test cleanup. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), mtu) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).Mtu().State(), 5*time.Second, mtu) + }() + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + setRequest1 := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{ + { + Path: resolvedMtuPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(9100), 10)), + }, + }, + }, + }, + } + setRequest2 := &gpb.SetRequest{ + Prefix: prefix, + Replace: []*gpb.Update{ + { + Path: resolvedMtuPath, + Val: &gpb.TypedValue{ + Value: &gpb.TypedValue_JsonIetfVal{ + JsonIetfVal: []byte(strconv.FormatUint(uint64(9122), 10)), + }, + }, + }, + }, + } + + ctx := context.Background() + newGNMIClient := func() gpb.GNMIClient { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + return gnmiClient + } + clients := map[string]gpb.GNMIClient{ + "c1": newGNMIClient(), + "c2": newGNMIClient(), + } + + eg, ctx := errgroup.WithContext(context.Background()) + eg.Go(func() error { + _, err := clients["c1"].Set(ctx, setRequest1) + return err + }) + eg.Go(func() error { + _, err := clients["c2"].Set(ctx, setRequest2) + return err + }) + if err := eg.Wait(); err != nil { + t.Fatalf("Error while calling Multiple Set API %v", err) + } +} + +/********************************************************** +* gNMI GET operations +**********************************************************/ +// Sample test that performs gNMI GET using subscribe once on state paths. +func TestGNMIGetPaths(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("75e16f69-06ce-4e9a-bdbb-af50339ca8c4").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // The Get() APIs in this test would panic if the switch does not return + // state value for the Openconfig path, resulting in a test failure. + // The test validates that the switch returns state values for the + // specified interface Openconfig path. + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Fetch port MTU. + mtu := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().State()) + t.Logf("MTU is %v", mtu) + + // Fetch /interfaces/interface[name=]/state subtree. + p := gnmi.Get(t, dut, gnmi.OC().Interface(intf).State()) + // All paths might not be present in the response. Therefore, validate members + // of GoStruct before accessing them. + if p.AdminStatus != oc.Interface_AdminStatus_UNSET { + t.Logf("admin-status: %v", p.AdminStatus) + } + if p.Enabled != nil { + t.Logf("enabled: %v", *p.Enabled) + } + if p.Mtu != nil { + t.Logf("mtu: %v", *p.Mtu) + } + if p.Id != nil { + t.Logf("ID: %v", *p.Id) + } + if p.HoldTime != nil { + h := *p.HoldTime + if h.Down != nil { + t.Logf("hold-time down: %v", *h.Down) + } + if h.Up != nil { + t.Logf("hold-time up: %v", *h.Up) + } + } + + // Fetch /interfaces/interface[name=]/config subtree. + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + // All paths might not be present in the response. Therefore, validate members + // of GoStruct before accessing them. + if res.AdminStatus != oc.Interface_AdminStatus_UNSET { + t.Logf("admin-status: %v", res.AdminStatus) + } + if res.Cpu != nil { + t.Logf("IsCpu: %v", *res.Cpu) + } + if res.Enabled != nil { + t.Logf("enabled: %v", *res.Enabled) + } + if res.Description != nil { + t.Logf("description: %v", *res.Description) + } +} + +// Test that performs gNMI GET at module level. +func TestGNMIGetModulePaths(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("66ae2d27-d50e-4012-8be7-5536eb43fae8").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + // Create getRequest message to fetch all components. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "components", + }, { + Name: "component", + }}, + }}, + Encoding: gpb.Encoding_PROTO, + } + + // Fetch raw gNMI client and call Get API to send Get Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Unable to fetch get client (%v)", err) + } + if getResp == nil { + t.Fatal("Get response is nil") + } + + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + updates := notifs[0].GetUpdate() + if len(updates) == 0 { + t.Fatalf("got %d updates in the notification, want >=1", len(updates)) + } + for i := range updates { + update := updates[i].GetPath() + // Go through all the paths to make sure they are working fine. + if _, err := ygot.PathToString(update); err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, prototext.Format(update)) + } + } +} + +// Test that performs gNMI GET at root level. +func TestGNMIGetRootPath(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("dcc2805e-8dda-4899-99ad-3f5a42f1985b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + // Create getRequest message to fetch root. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Encoding: gpb.Encoding_PROTO, + } + + // Fetch raw gNMI client and call Get API to send Get Request. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Unable to fetch get client (%v)", err) + } + if getResp == nil { + t.Fatal("Get response is nil") + } + + notifs := getResp.GetNotification() + if len(notifs) < 6 { + t.Fatalf("got %d notifications, want >= 6", len(notifs)) + } + for updates := range notifs { + updates := notifs[updates].GetUpdate() + if len(updates) < 1 { + continue + } + for i := range updates { + update := updates[i].GetPath() + // Go through all the paths to make sure they are working fine. + if _, err := ygot.PathToString(update); err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, prototext.Format(update)) + } + } + } +} + +func TestGnmiProtoEncodingGet(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("0c25a72c-9b1a-4f80-90eb-177924f02802").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Create getRequest message with ASCII encoding. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "interfaces", + }, { + Name: "interface", + Key: map[string]string{"name": intf}, + }}, + }}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Unable to fetch get client (%v)", err) + } + if getResp == nil { + t.Fatalf("Unable to fetch get client, get response is nil") + } + + notifs := getResp.GetNotification() + if len(notifs) != 1 { + t.Fatalf("got %d notifications, want 1", len(notifs)) + } + updates := notifs[0].GetUpdate() + if len(updates) < 1 { + t.Fatalf("got %d updates in the notification, want >1", len(updates)) + } + for i := range updates { + update := updates[i].GetPath() + // Go through all the paths to make sure they are working fine. + pathStr, err := ygot.PathToString(update) + t.Logf("pathStr: %v", pathStr) + if err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, update) + } + } + +} + +func TestGnmiInvalidKeyGet(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("ca3d9356-ca81-482b-aa36-8be5ac9180ba").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Create getRequest message with ASCII encoding. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "interfaces", + }, { + Name: "interface", + Key: map[string]string{"name": "EthernetY"}, + }}, + }}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Get(ctx, getRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } +} + +func TestGnmiInvalidPathGet(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("8f834ec5-da76-4884-9d84-58fc687c4f8c").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Create getRequest message with ASCII encoding. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "xyz", + }}, + }}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err := gnmiClient.Get(ctx, getRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } +} + +func TestGnmiAsciiEncodingGet(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("104a2dbf-fb8d-40af-a592-99e2bfd868d2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + // Select a random front panel interface EthernetX. + intf, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + // Create getRequest message with ASCII encoding. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "interfaces", + }, { + Name: "interface", + Key: map[string]string{"name": intf}, + }}, + }}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_ASCII, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + if _, err = gnmiClient.Get(ctx, getRequest); err == nil { + t.Fatalf("Set request is expected to fail but it didn't") + } +} + +// Test that performs gNMI SET Replace at root level. +// This test will fail till the binding issue for root path is fixed (b/200096572) +func TestGNMISetReplaceRootPath(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("09df0cd9-3e23-4f8c-8a0b-9105de3a83af").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + if err := testhelper.ConfigPush(t, dut, nil); err != nil { + t.Fatalf("Failed to push config: %v", err) + } + + // Select a random front panel interface EthernetX. + var intf string + + info, err := testhelper.FetchPortsOperStatus(t, dut) + if err != nil { + t.Fatalf("Failed to fetch port operation status: %v", err) + } + + if len(info.Up) == 0 { + t.Fatalf("Failed to fetch port with operation status UP: %v", err) + } + + for _, port := range info.Up { + if isParent, err := testhelper.IsParentPort(t, dut, port); err == nil && isParent { + intf = port + break + } + } + + // Get fields from interface subtree. + if intf != "" { + intfIDAfterSet := gnmi.Get(t, dut, gnmi.OC().Interface(intf).State()).GetId() + t.Logf("ID After Set is %v for interface %v", intfIDAfterSet, intf) + } else { + t.Fatalf("Failed to fetch valid parent interface.") + } +} diff --git a/tests/gnmi_stress_test.go b/tests/gnmi_stress_test.go new file mode 100644 index 0000000..1c5b2ed --- /dev/null +++ b/tests/gnmi_stress_test.go @@ -0,0 +1,618 @@ +package gnmi_stress_test + +import ( + "context" + "fmt" + "math/rand" + "sync" + "testing" + "time" + + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + gst "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/tests/gnmi_stress_helper" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ygot/ygot" + "google.golang.org/grpc" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// gNMI load test - Replacing a single leaf 100 times. +func TestGNMILoadTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("f5e40be6-9913-4926-8d69-505e51f566f1").Teardown(t) + dut := ondatra.DUT(t, "DUT") + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + oldMtu := gnmi.Get(t, dut, gnmi.OC().Interface(port).Mtu().Config()) + gst.SanityCheck(t, dut, port) + + for i := gst.MinMtuStepInc; i < gst.MaxMtuStepInc; i++ { + // Configure port MTU and verify that state path reflects configured MTU. + mtu := uint16(1500 + i) + gnmi.Replace(t, dut, gnmi.OC().Interface(port).Mtu().Config(), mtu) + gst.CollectPerformanceMetrics(t, dut) + got := gnmi.Get(t, dut, gnmi.OC().Interface(port).Mtu().Config()) + if got != mtu { + t.Errorf("MTU matched failed! got:%v, want:%v", got, mtu) + } + } + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + // Replace the old MTU value as a test cleanup. + gnmi.Replace(t, dut, gnmi.OC().Interface(port).Mtu().Config(), oldMtu) + gst.SanityCheck(t, dut, port) + +} + +// gNMI load test short interval(30 minutes). +func TestGNMIShortStressTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("44fa854f-5d85-42aa-9ad0-4ee8dbce7f10").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressTestHelper(t, dut, gst.ShortStressTestInterval) + gst.SanityCheck(t, dut) +} + +// gNMI broken client test +func TestGNMIBrokenClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("cd36ba68-a2c1-485a-bc1a-c79463ed80d9").Teardown(t) + dut := ondatra.DUT(t, "DUT") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + gst.SanityCheck(t, dut) + for i := 0; i < gst.MinIteration; i++ { + // Create getRequest message with ASCII encoding. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{{ + Elem: []*gpb.PathElem{{ + Name: "interfaces", + }}, + }}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(context.Background(), grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err == nil { + t.Logf("GetResponse:\n%v", getResp) + t.Fatalf("The getRequest is successfully received on broken client") + } + if getResp != nil { + t.Fatalf("getResponse is received successfully") + } + } + gst.SanityCheck(t, dut) +} + +// gNMI different leaf get test +func TestGNMIGetDifferentLeafTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("08f9ffba-54a9-4d47-a3dc-0e4420fe296b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + gst.CollectPerformanceMetrics(t, dut) + for i := 0; i < gst.AvgIteration; i++ { + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + reqPath := fmt.Sprintf(gst.Path[rand.Intn(len(gst.Path))], port) + // Create Get Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: paths, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + + if getResp == nil { + t.Fatalf("Get response is nil") + } + t.Logf("GetResponse:\n%v", getResp) + gst.CollectPerformanceMetrics(t, dut) + } + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + gst.SanityCheck(t, dut) +} + +// gNMI different subtrees get test +func TestGNMIGetDifferentSubtreeTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("357762b4-4d34-467e-b321-90a2d271d50d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + gst.CollectPerformanceMetrics(t, dut) + for i := 0; i < gst.MinIteration; i++ { + reqPath := gst.Subtree[rand.Intn(len(gst.Subtree))] + // Create Get Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{sPath}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + + if getResp == nil { + t.Fatalf("Get response is nil") + } + t.Logf("GetResponse:\n%v", getResp) + gst.CollectPerformanceMetrics(t, dut) + } + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + gst.SanityCheck(t, dut) +} + +// gNMI different Client get test +func TestGNMIGetDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SanityCheck(t, dut) + ctx := context.Background() + newGNMIClient := func() gpb.GNMIClient { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + return gnmiClient + } + clients := map[string]gpb.GNMIClient{ + "c1": newGNMIClient(), + "c2": newGNMIClient(), + "c3": newGNMIClient(), + "c4": newGNMIClient(), + "c5": newGNMIClient(), + "c6": newGNMIClient(), + "c7": newGNMIClient(), + "c8": newGNMIClient(), + "c9": newGNMIClient(), + "c10": newGNMIClient(), + } + + var wg sync.WaitGroup + for k := range clients { + v := k + wg.Add(1) + + rand.Seed(time.Now().Unix()) + gst.CollectPerformanceMetrics(t, dut) + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + reqPath := fmt.Sprintf(gst.Path[rand.Intn(len(gst.Path))], port) + // Create Get Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{sPath}, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + go func() { + getResp, err := clients[v].Get(ctx, getRequest) + if err != nil { + t.Log("Error while calling Get Raw API") + } + if getResp == nil { + t.Log("Get response is nil") + } + t.Logf("GetResponse:\n%v", getResp) + wg.Done() + }() + + gst.CollectPerformanceMetrics(t, dut) + } + wg.Wait() + t.Logf("After 10 seconds of idle time, the performance metrics are:") + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + gst.SanityCheck(t, dut) +} + +// gNMI different leaf get test +func TestGNMISetUpdateDifferentLeafTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("08f9ffba-54a9-4d47-a3dc-0e4420fe296b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressSetTestHelper(t, dut, gst.AvgIteration, false) +} + +// gNMI different leaf set update test +func TestGNMISetReplaceDifferentLeafTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("08f9ffba-54a9-4d47-a3dc-0e4420fe296b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressSetTestHelper(t, dut, gst.AvgIteration, true) +} + +// gNMI different leaf set replace test +func TestGNMISetUpdateDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SetDifferentClientTest(t, dut, false) +} + +// gNMI different leaf set update test +func TestGNMISetReplaceDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SetDifferentClientTest(t, dut, true) +} + +// gNMI different leaf set delete test +func TestGNMISetDeleteDifferentLeafTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("08f9ffba-54a9-4d47-a3dc-0e4420fe296b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + gst.CollectPerformanceMetrics(t, dut) + for i := 0; i < gst.AvgIteration; i++ { + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + gst.SetDefaultValuesHelper(t, dut, port) + sPath, err := ygot.StringToStructuredPath(gst.RandomDeletePath(port)) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + + paths := []*gpb.Path{sPath} + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: paths, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_JSON_IETF, + } + t.Logf("GetRequest:\n%v", getRequest) + + // Fetch get client using the raw gNMI client. + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + t.Logf("GetResponse:\n%v", getResp) + + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Delete: []*gpb.Path{sPath}, + } + t.Logf("SetRequest:\n%v", setRequest) + + // Fetch get client using the raw gNMI client. + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + + if setResp == nil { + t.Fatalf("set response is nil") + } + t.Logf("setResponse:\n%v", setResp) + gst.CollectPerformanceMetrics(t, dut) + + // Restore the old values for the path + + if getResp != nil { + updates, err := gst.UpdatesWithJSONIETF(getResp) + if err != nil { + t.Fatalf("Unable to get updates with JSON IETF: (%v)", err) + } + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: updates, + } + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to restore the original value using set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + } + } + t.Logf("After %v seconds of idle time, collecting performance metrics", gst.IdleTime) + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + gst.SanityCheck(t, dut) +} + +// gNMI different subtrees set delete test +func TestGNMISetDeleteDifferentSubtreeTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("357762b4-4d34-467e-b321-90a2d271d50d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SanityCheck(t, dut) + rand.Seed(time.Now().Unix()) + gst.CollectPerformanceMetrics(t, dut) + // Restore the config on the DUT after the test. + defer func() { + if err := testhelper.ConfigPush(t, dut, nil); err != nil { + t.Fatalf("Failed to restore config: %v", err) + } + }() + for i := 0; i < gst.MinIteration; i++ { + reqPath := gst.DelSubtree[rand.Intn(len(gst.DelSubtree))] + + // Create Set Delete Request. + sPath, err := ygot.StringToStructuredPath(reqPath) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: []*gpb.Path{sPath}, + Type: gpb.GetRequest_CONFIG, + Encoding: gpb.Encoding_JSON_IETF, + } + t.Logf("GetRequest:\n%v", getRequest) + + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + t.Logf("GetResponse:\n%v", getResp) + + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Delete: []*gpb.Path{sPath}, + } + t.Logf("SetRequest:\n%v", setRequest) + + // Fetch set delete client using the raw gNMI client. + setResp, err := gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + if setResp == nil { + t.Fatalf("set response is nil") + } + t.Logf("setResponse:\n%v", setResp) + + // Restore the old values. + // A defer statement was not used to restore the values because this is in a for loop. The for + // loop deletes a subtree and then restores the values after. The subtree that is chosen could + // be chosen multiple times in the same test, so a defer would not work in this case. + updates, err := gst.UpdatesWithJSONIETF(getResp) + if err != nil { + t.Fatalf("Unable to get updates with JSON IETF: (%v)", err) + } + setRequest = &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Replace: updates, + } + setResp, err = gnmiClient.Set(ctx, setRequest) + if err != nil { + t.Fatalf("Unable to restore the original value using set client (%v)", err) + } + t.Logf("SetResponse:\n%v", setResp) + gst.CollectPerformanceMetrics(t, dut) + } + t.Logf("After %v seconds of idle time, the performance metrics are collected", gst.IdleTime) + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + gst.SanityCheck(t, dut) +} + +// gNMI different Client set delete test +func TestGNMISetDeleteDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SanityCheck(t, dut) + ctx := context.Background() + newGNMIClient := func() gpb.GNMIClient { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + return gnmiClient + } + clients := map[string]gpb.GNMIClient{ + "c1": newGNMIClient(), + "c2": newGNMIClient(), + "c3": newGNMIClient(), + "c4": newGNMIClient(), + "c5": newGNMIClient(), + "c6": newGNMIClient(), + "c7": newGNMIClient(), + "c8": newGNMIClient(), + "c9": newGNMIClient(), + "c10": newGNMIClient(), + } + info, err := testhelper.FetchPortsOperStatus(t, dut) + if err != nil || info == nil { + t.Fatalf("Failed to fetch ports oper-status: %v", err) + } + interfaces := info.Up + numIntfs := len(interfaces) + var port string + + var wg sync.WaitGroup + for k := range clients { + if len(interfaces) == 0 { + t.Logf("Less operationally up interfaces than clients: %v interfaces, %v clients", numIntfs, len(clients)) + break + } + v := k + wg.Add(1) + + rand.Seed(time.Now().Unix()) + gst.CollectPerformanceMetrics(t, dut) + port, interfaces = interfaces[0], interfaces[1:] + gst.SetDefaultValuesHelper(t, dut, port) + sPath, err := ygot.StringToStructuredPath(gst.RandomDeletePath(port)) + if err != nil { + t.Fatalf("Unable to convert string to path (%v)", err) + } + + paths := []*gpb.Path{sPath} + + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Path: paths, + Type: gpb.GetRequest_ALL, + Encoding: gpb.Encoding_PROTO, + } + t.Logf("GetRequest:\n%v", getRequest) + + setRequest := &gpb.SetRequest{ + Prefix: &gpb.Path{Origin: "openconfig", Target: dut.Name()}, + Delete: []*gpb.Path{sPath}, + } + t.Logf("SetRequest:\n%v", setRequest) + ctx := context.Background() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + t.Fatalf("Error while calling Get Raw API: (%v)", err) + } + t.Logf("GetResponse:\n%v", getResp) + + // Fetch get client using the raw gNMI client. + go func() { + // Fetch get client using the raw gNMI client. + setResp, err := clients[v].Set(context.Background(), setRequest) + if err != nil { + t.Log("Error while calling Set delete Raw API") + } + if setResp == nil { + t.Log("Set response is nil") + } + t.Logf("setResponse:\n%v", setResp) + wg.Done() + }() + gst.CollectPerformanceMetrics(t, dut) + // Restore the old values for the path + gst.SetDefaultValuesHelper(t, dut, port) + } + wg.Wait() + t.Logf("After %v seconds of idle time, colecting performance metrics", gst.IdleTime) + time.Sleep(gst.IdleTime * time.Second) + gst.CollectPerformanceMetrics(t, dut) + gst.SanityCheck(t, dut) +} + +// gNMI different leaf subscription poll mode test +func TestGNMISubscribePollDifferentLeafTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("08f9ffba-54a9-4d47-a3dc-0e4420fe296b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressTestSubsHelper(t, dut, false, true) +} + +// gNMI different subtree subscription poll mode test +func TestGNMISubscribePollDifferentSubtreeTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("357762b4-4d34-467e-b321-90a2d271d50d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressTestSubsHelper(t, dut, true, true) +} + +// gNMI different Client Subscribe Poll test +func TestGNMISubscribePollDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SubscribeDifferentClientTest(t, dut, true) +} + +// gNMI different leaf subscription Sample mode test +func TestGNMISubscribeSampleDifferentLeafTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("08f9ffba-54a9-4d47-a3dc-0e4420fe296b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressTestSubsHelper(t, dut, false, false) +} + +// gNMI different subtree subscription Sample mode test +func TestGNMISubscribeSampleDifferentSubtreeTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("357762b4-4d34-467e-b321-90a2d271d50d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.StressTestSubsHelper(t, dut, true, false) +} + +// gNMI different Client Subscribe Sample test +func TestGNMISubscribeSampleDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.SubscribeDifferentClientTest(t, dut, false) +} + +// gNMI different Client random operations test +func TestGNMIRandomOpsDifferentClientTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("389641b7-d995-4411-a222-e38caa9291a2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + gst.RandomDifferentClientTestHelper(t, dut, gst.ShortStressTestInterval) +} diff --git a/tests/gnmi_subscribe_modes_test.go b/tests/gnmi_subscribe_modes_test.go new file mode 100644 index 0000000..cc26742 --- /dev/null +++ b/tests/gnmi_subscribe_modes_test.go @@ -0,0 +1,1037 @@ +package gnmi_subscribe_modes_test + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ygot/ygot" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/prototext" +) + +const ( + deleteTreePath = "/system/config/" + deletePath = "/system/config/hostname" + timePath = "/system/state/current-datetime" + nodePath = "/system/state/hostname" + subTreePath = "/system/state" + containerPath = "/components" + rootPath = "/" + onChangePath = "/interfaces/interface[name=%s]/state/mtu" + errorResponse = "expectedError" + syncResponse = "expectedSync" + + shortTime = 5 * time.Second + mediumTime = 10 * time.Second + longTime = 30 * time.Second +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +type subscribeTest struct { + uuid string + reqPath string + mode gpb.SubscriptionList_Mode + updatesOnly bool + subMode gpb.SubscriptionMode + sampleInterval uint64 // nanoseconds + suppressRedundant bool + heartbeatInterval uint64 // nanoseconds + expectError bool + timeout time.Duration +} + +type operStatus struct { + match bool + delete bool + value string +} + +func ignorePaths(path string) bool { + // Paths that change during root and container level tests + subPaths := []string{ + // TODO Check back if lb/bond are needed after this bug is corrected. + "/ethernet/state/counters/", + "//interfaces/interface[name=Loopback0]", + "//interfaces/interface[name=bond0]", + "//qos/interfaces/interface", + "//snmp/engine/version/", + "//system/mount-points/mount-point", + "//system/processes/process", + "//system/cpus/cpu", + "//system/crm/threshold", + "//system/ntp/", + "//system/memory/", + "//system/ssh-server/ssh-server-vrfs", + "/subinterface[index=0]/ipv4/unnumbered/", + "/subinterface[index=0]/ipv4/sag-ipv4/", + "/subinterface[index=0]/ipv6/sag-ipv6/", + "/gnmi-pathz-policy-counters/paths/path", + "/system/state/boot-time", + "/system/state/uptime", + } + + for _, sub := range subPaths { + if strings.Contains(path, sub) { + return true + } + } + return false +} + +var skipTest = map[string]bool{ + // TODO Ondatra fails to delete subtree + "TestGNMISubscribeModes/subscribeDeleteNodeLevel": true, + "TestGNMISubscribeModes/subscribeDeleteSubtreeLevel": true, +} + +func TestGNMISubscribeModes(t *testing.T) { + testCases := []struct { + name string + function func(*testing.T) + }{ + { + "subscribeOnChange", + subscribeTest{ + uuid: "f3c55aed-6522-458d-a3cb-e9eca005bcf1", + reqPath: onChangePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_ON_CHANGE, + timeout: shortTime, + }.subModeOnChangeTest, + }, + { + "subscribeOnChangeHeartbeatInterval", + subscribeTest{ + uuid: "5defcb39-7ffa-4404-8eab-59499b50796e", + reqPath: onChangePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_ON_CHANGE, + heartbeatInterval: 2000000000, + timeout: shortTime, + }.subModeOnChangeTest, + }, + { + "subscribeOnChangeDefinedNode", + subscribeTest{ + uuid: "1092dc1a-42c8-4125-b2a0-64596dc340ab", + reqPath: onChangePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_TARGET_DEFINED, + timeout: shortTime, + }.subModeOnChangeTest, + }, + { // TODO UMF returns timeout instead of returning proper error code. + "subscribeOnChangeUnsupportedPath", + subscribeTest{ + uuid: "c003d854-6b41-4b0d-acdf-4cc77bd02252", + reqPath: subTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_ON_CHANGE, + sampleInterval: 2000000000, + expectError: true, + timeout: shortTime, + }.subModeOnChangeTest, + }, + { // TODO Updates_Only is not filtering properly. + "subscribeOnChangeUpdatesOnly", + subscribeTest{ + uuid: "a242c00e-74e7-4749-83cf-9ee724c64901", + reqPath: onChangePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_ON_CHANGE, + updatesOnly: true, + timeout: shortTime, + }.subModeOnChangeTest, + }, + { + "subscribeOnceRootLevel", + subscribeTest{ + uuid: "3507ab19-ffb9-4e30-8958-0bb2dc80b424", + reqPath: rootPath, + mode: gpb.SubscriptionList_ONCE, + timeout: longTime, + }.subModeOnceTest, + }, + { + "subscribeOnceContainerLevel", + subscribeTest{ + uuid: "42b4af42-2394-4945-b094-2a1130d2002d", + reqPath: containerPath, + mode: gpb.SubscriptionList_ONCE, + timeout: mediumTime, + }.subModeOnceTest, + }, + { + "subscribeOnceSubtreeLevel", + subscribeTest{ + uuid: "349ef06f-eeaa-45f0-b463-86505dc57131", + reqPath: subTreePath, + mode: gpb.SubscriptionList_ONCE, + timeout: shortTime, + }.subModeOnceTest, + }, + { + "subscribeOnceNodeLevel", + subscribeTest{ + uuid: "bc3c26cc-259c-4b98-8b96-8f98e084724c", + reqPath: nodePath, + mode: gpb.SubscriptionList_ONCE, + timeout: shortTime, + }.subModeOnceTest, + }, + { + "subscribePollRootLevel", + subscribeTest{ + uuid: "c658fc60-bd58-4fcc-970c-b994a0cf0e94", + reqPath: rootPath, + mode: gpb.SubscriptionList_POLL, + timeout: longTime, + }.subModePollTest, + }, + { + "subscribePollContainerLevel", + subscribeTest{ + uuid: "5f424b35-4d7f-44db-a4c2-0bd6f2370301", + reqPath: containerPath, + mode: gpb.SubscriptionList_POLL, + timeout: mediumTime, + }.subModePollTest, + }, + // TODO Updates_Only is not filtering properly. + { + "subscribeOnceUpdatesOnly", + subscribeTest{ + uuid: "88b334bd-e835-4cb9-975f-e7b01bd6e1bf", + reqPath: subTreePath, + mode: gpb.SubscriptionList_ONCE, + updatesOnly: true, + timeout: shortTime, + }.subModeUpdatesTest, + }, + { + "subscribePollUpdatesOnly", + subscribeTest{ + uuid: "177c8d8d-a51b-448d-96b1-ed3e1dde0629", + reqPath: subTreePath, + mode: gpb.SubscriptionList_POLL, + updatesOnly: true, + timeout: shortTime, + }.subModeUpdatesTest, + }, + { + "subscribeSampleUpdatesOnly", + subscribeTest{ + uuid: "ebb593da-4f24-4394-80b9-4463a96843bb", + reqPath: subTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + timeout: shortTime, + updatesOnly: true, + }.subModeUpdatesTest, + }, + { + "subscribePollSubtreeLevel", + subscribeTest{ + uuid: "d298894b-3110-4bb3-b13f-3e572d57791e", + reqPath: subTreePath, + mode: gpb.SubscriptionList_POLL, + timeout: shortTime, + }.subModePollTest, + }, + { + "subscribePollNodeLevel", + subscribeTest{ + uuid: "cb622b6e-5142-4c59-a6b9-603b45b8bcab", + reqPath: nodePath, + mode: gpb.SubscriptionList_POLL, + timeout: shortTime, + }.subModePollTest, + }, + { + "subscribeSampleSubtreeLevel", + subscribeTest{ + uuid: "899345da-b715-4caa-a02f-2d03d18c233e", + reqPath: subTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + timeout: shortTime, + }.subModeSampleTest, + }, + { // TODO UMF returns timeout instead of returning proper error code. + "subscribeSampleInvalidInterval", + subscribeTest{ + uuid: "8cf7ea62-5bda-4f71-bd86-fdce88ba2753", + reqPath: nodePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 1, + expectError: true, + timeout: shortTime, + }.subModeSampleTest, + }, + { + "subscribeSampleDefinedNode", + subscribeTest{ + uuid: "45305a9a-c602-421f-8f6a-21d520fea9f8", + reqPath: nodePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_TARGET_DEFINED, + sampleInterval: 2000000000, + timeout: shortTime, + }.subModeSampleTest, + }, + { + "subscribeMixedDefinedNode", + subscribeTest{ + uuid: "ae4c435a-9fa7-494a-94f7-75cd662c3d95", + reqPath: subTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_TARGET_DEFINED, + sampleInterval: 2000000000, + timeout: shortTime, + }.subModeSampleTest, + }, + { + "subscribeSampleRootLevel", + subscribeTest{ + uuid: "a495d0b5-482e-411b-9bac-2baa79776293", + reqPath: rootPath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 5000000000, + timeout: longTime, + }.subModeRootTest, + }, + { + "subscribeSampleContainerLevel", + subscribeTest{ + uuid: "aeff11b5-aee2-4689-85e0-a124b5d73506", + reqPath: containerPath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 5000000000, + timeout: longTime, + }.subModeSampleTest, + }, + { + "subscribeSampleNodeLevel", + subscribeTest{ + uuid: "880b3893-da72-44c5-998c-013f0303969f", + reqPath: nodePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + timeout: shortTime, + }.subModeSampleTest, + }, + { + "subscribeDeleteNodeLevel", + subscribeTest{ + uuid: "529f58c0-8b9b-4820-aeb6-94feb1a68198", + reqPath: deletePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + timeout: shortTime, + }.subModeDeleteTest, + }, + { + "subscribeDeleteSubtreeLevel", + subscribeTest{ + uuid: "e9d932d5-5fa5-4e00-8c60-cd8823fc34b2", + reqPath: deleteTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + timeout: shortTime, + }.subModeDeleteTest, + }, + { + "subscribeSampleSuppressRedundant", + subscribeTest{ + uuid: "5c6e0713-cb8d-43a5-bd8e-8a1ac395eab6", + reqPath: subTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + suppressRedundant: true, + timeout: shortTime, + }.subModeSuppressTest, + }, + { + "subscribeSampleHeartbeat", + subscribeTest{ + uuid: "8b58ff99-39fc-41e8-9589-b78da0aeca12", + reqPath: subTreePath, + mode: gpb.SubscriptionList_STREAM, + subMode: gpb.SubscriptionMode_SAMPLE, + sampleInterval: 2000000000, + suppressRedundant: true, + heartbeatInterval: 3000000000, + timeout: shortTime, + }.subModeSuppressTest, + }, + } + + dut := ondatra.DUT(t, "DUT") + + // Check if the switch is responsive with Get API, which will panic if the switch does not return + // state value for specified interface Openconfig path resulting in a test failure. + intf, err := testhelper.RandomInterface(t, dut, &testhelper.RandomInterfaceParams{OperDownOk: true}) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().State()) + + for _, testCase := range testCases { + t.Run(testCase.name, testCase.function) + } +} + +// Test for gNMI Subscribe Stream mode for OnChange subscriptions. +func (c subscribeTest) subModeOnChangeTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + var intf string + if c.expectError == false { + var err error + intf, err = testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + c.reqPath = fmt.Sprintf(c.reqPath, intf) + } + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := make(map[string]operStatus) + if !c.updatesOnly { + expectedPaths = c.buildExpectedPaths(t, dut) + } + expectedPaths[syncResponse] = operStatus{} + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if c.expectError { + foundErr, ok := foundPaths[errorResponse] + if !ok { + t.Fatal("Expected error but got none") + } + if !strings.Contains(foundErr.value, "InvalidArgument") { + t.Errorf("Error is not an InvalidArgument: %s", foundErr.value) + } + return + } + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + delete(expectedPaths, syncResponse) + + got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Mtu().State()) + defer gnmi.Update(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), got) + mtu := uint16(1500) + if got == 1500 { + mtu = 9000 + } + if c.heartbeatInterval == 0 { + gnmi.Update(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), mtu) + } + + foundPaths, delay := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + if c.heartbeatInterval != 0 { + if delay > time.Duration(c.heartbeatInterval+(c.heartbeatInterval/2)) { + t.Errorf("Failed sampleInterval with time of %v", delay) + } + gnmi.Update(t, dut, gnmi.OC().Interface(intf).Mtu().Config(), mtu) + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + } +} + +// Test for gNMI Subscriptions with UpdatesOnly flag. +func (c subscribeTest) subModeUpdatesTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := make(map[string]operStatus) + + if c.mode != gpb.SubscriptionList_POLL { + expectedPaths = c.buildExpectedPaths(t, dut) // TODO remove once fixed + } + expectedPaths[syncResponse] = operStatus{} + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + if c.mode == gpb.SubscriptionList_ONCE { + return + } + + if c.mode == gpb.SubscriptionList_POLL { + subscribeClient.Send(&gpb.SubscribeRequest{Request: &gpb.SubscribeRequest_Poll{}}) + } + + expectedPaths = c.buildExpectedPaths(t, dut) + + foundPaths, delay := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + if c.mode == gpb.SubscriptionList_STREAM { + if delay > time.Duration(c.sampleInterval+(c.sampleInterval/2)) { + t.Errorf("Failed sampleInterval with time of %v", delay) + } + } +} + +// Test for gNMI Subscribe Stream mode for Sample subscriptions with suppression. +func (c subscribeTest) subModeSuppressTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := c.buildExpectedPaths(t, dut) + expectedPaths[syncResponse] = operStatus{} + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%v", expectedPaths, diff) + } + + updatedPaths := map[string]operStatus{timePath: operStatus{}} + + foundPaths, delay := collectResponse(t, subscribeClient, updatedPaths) + if diff := cmp.Diff(updatedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(updatedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", updatedPaths, diff) + } + if delay > time.Duration(c.sampleInterval+(c.sampleInterval/2)) { + t.Errorf("Failed sampleInterval with time of %v", delay) + } + + if c.heartbeatInterval != 0 { + delete(expectedPaths, syncResponse) + foundPaths, delay := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + if delay > time.Duration(c.heartbeatInterval+(c.heartbeatInterval/2)) { + t.Errorf("Failed heartbeatInterval with time of %v", delay) + } + } +} + +// Test for gNMI Subscribe Stream mode for Sample subscriptions. +func (c subscribeTest) subModeSampleTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := c.buildExpectedPaths(t, dut) + expectedPaths[syncResponse] = operStatus{} + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if c.expectError { + foundErr, ok := foundPaths[errorResponse] + if !ok { + t.Fatal("Expected error but got none") + } + if !strings.Contains(foundErr.value, "InvalidArgument") { + t.Errorf("Error is not an InvalidArgument: %s", foundErr.value) + } + return + } + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths): \nResponse mismatch (-missing +extra):\n%s", diff) + } + + delete(expectedPaths, syncResponse) + foundPaths, delay := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths): \nResponse mismatch (-missing +extra):\n%s", diff) + } + // Allow for plus roughly 50% of the sample interval + // because root level requests have much longer delays + if delay > time.Duration(c.sampleInterval+(c.sampleInterval/2)) { + t.Errorf("Failed sampleInterval with time of %v", delay) + } +} + +// Test for gNMI Subscribe Once mode for different levels. +func (c subscribeTest) subModeOnceTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := c.buildExpectedPaths(t, dut) + expectedPaths[syncResponse] = operStatus{} + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } +} + +// Test for gNMI Subscribe Stream mode node deletions. +func (c subscribeTest) subModeDeleteTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := c.buildExpectedPaths(t, dut) + expectedPaths[syncResponse] = operStatus{} + + gotName := gnmi.Get(t, dut, gnmi.OC().System().Hostname().State()) + defer gnmi.Update(t, dut, gnmi.OC().System().Hostname().Config(), gotName) + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + + if c.reqPath == deletePath { + gnmi.Delete(t, dut, gnmi.OC().System().Hostname().Config()) + } + if c.reqPath == deleteTreePath { + gnmi.Delete(t, dut, gnmi.OC().System().Config()) + } + delete(expectedPaths, syncResponse) + for _, v := range expectedPaths { + v.delete = true + } + + foundPaths, delay := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + if delay > time.Duration(c.sampleInterval+(c.sampleInterval/2)) { + t.Errorf("Failed sampleInterval with time of %v", delay) + } +} + +// Test for gNMI Subscribe Poll mode for different levels. +func (c subscribeTest) subModePollTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + subscribeRequest := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(subscribeRequest)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(subscribeRequest); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + expectedPaths := c.buildExpectedPaths(t, dut) + expectedPaths[syncResponse] = operStatus{} + + foundPaths, _ := collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } + + delete(expectedPaths, syncResponse) + subscribeClient.Send(&gpb.SubscribeRequest{Request: &gpb.SubscribeRequest_Poll{}}) + foundPaths, _ = collectResponse(t, subscribeClient, expectedPaths) + if diff := cmp.Diff(expectedPaths, foundPaths, cmpopts.IgnoreUnexported(operStatus{})); diff != "" { + t.Errorf("collectResponse(expectedPaths):\n%v \nResponse mismatch (-missing +extra):\n%s", expectedPaths, diff) + } +} + +func (c subscribeTest) subModeRootTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + if skipTest[t.Name()] { + t.Skip() + } + dut := ondatra.DUT(t, "DUT") + + req := buildRequest(t, c, dut.Name()) + t.Logf("SubscribeRequest:\n%v", prototext.Format(req)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + subscribeClient, err := gnmiClient.Subscribe(ctx) + if err != nil { + t.Fatalf("Unable to get subscribe client (%v)", err) + } + + if err := subscribeClient.Send(req); err != nil { + t.Fatalf("Failed to send gNMI subscribe request (%v)", err) + } + + // First listener returns after sync response + if err := clientListener(t, subscribeClient); err != nil { + t.Errorf("Initial Response failed (%v)", err) + } + + // Second listener returns after fixed time with no errors + if err := clientListener(t, subscribeClient); err != nil { + t.Errorf("Subscribe Response failed (%v)", err) + } + +} + +func collectResponse(t *testing.T, subClient gpb.GNMI_SubscribeClient, expectedPaths map[string]operStatus) (map[string]operStatus, time.Duration) { + t.Helper() + start := time.Now() + // Process response from DUT. + expectedCount := len(expectedPaths) + foundPaths := make(map[string]operStatus) + for pCount := 0; pCount < expectedCount; { + // Wait for response from DUT. + done := make(chan struct{}) + resCh := make(chan *gpb.SubscribeResponse, 1) + errCh := make(chan error, 1) + go func(subClient gpb.GNMI_SubscribeClient, resCh chan<- *gpb.SubscribeResponse, errCh chan<- error) { + res, err := subClient.Recv() + close(done) + resCh <- res + errCh <- err + }(subClient, resCh, errCh) + timer := time.NewTimer(mediumTime) + select { + case <-timer.C: + t.Fatalf("Timed out waiting on stream, expected: \n%+v, \nfound: \n%+v", expectedPaths, foundPaths) + case <-done: + if !timer.Stop() { + <-timer.C + } + } + res := <-resCh + err := <-errCh + if err != nil { + if _, ok := expectedPaths[errorResponse]; ok { + foundPaths[errorResponse] = operStatus{ + match: true, + value: err.Error(), + } + return foundPaths, 0 + } + t.Fatalf("Response error received from DUT (%v)", err) + } + switch v := res.Response.(type) { + case *gpb.SubscribeResponse_Update: + // Process Update message received in SubscribeResponse. + updates := v.Update + prefixStr, err := ygot.PathToString(updates.GetPrefix()) + if err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, updates.GetPrefix()) + } + for _, d := range updates.GetDelete() { + elemStr, err := ygot.PathToString(d) + if err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, d) + } + + pathStr := prefixStr + elemStr + if !ignorePaths(pathStr) { + _, ok := expectedPaths[syncResponse] + foundPaths[pathStr] = operStatus{match: ok, delete: true} + pCount++ + } + } + + // Perform basic sanity on the Update message. + for _, update := range updates.GetUpdate() { + if update.Path == nil { + t.Fatalf("Invalid nil Path in update: %v", prototext.Format(update)) + } + if update.Val == nil { + t.Fatalf("Invalid nil Val in update: %v", prototext.Format(update)) + } + // Path is partially present in Prefix and partially in Update in the response. + elemStr, err := ygot.PathToString(update.Path) + if err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, update.Path) + } + pathStr := prefixStr + elemStr + + if !ignorePaths(pathStr) { + _, ok := expectedPaths[syncResponse] + foundPaths[pathStr] = operStatus{ + match: ok, + value: update.GetVal().GetStringVal(), + } + pCount++ + } + } + + case *gpb.SubscribeResponse_SyncResponse: + _, ok := expectedPaths[syncResponse] + foundPaths[syncResponse] = operStatus{match: ok} + pCount++ + } + } + return foundPaths, time.Since(start) +} + +func clientListener(t *testing.T, sc gpb.GNMI_SubscribeClient) error { + t.Helper() + timeout := time.After(mediumTime) + for { + select { + case <-timeout: + return nil + default: + m, err := sc.Recv() + if err != nil { + if errors.Is(err, context.Canceled) { + return nil + } + return err + } + switch m.Response.(type) { + case *gpb.SubscribeResponse_SyncResponse: + return nil + } + } + } +} + +func (c *subscribeTest) buildExpectedPaths(t *testing.T, dut *ondatra.DUTDevice) map[string]operStatus { + t.Helper() + expectedPaths := make(map[string]operStatus) + if c.expectError { + expectedPaths[errorResponse] = operStatus{} + return expectedPaths + } + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + + resolvedPath, errs := ygot.StringToStructuredPath(c.reqPath) + if errs != nil { + t.Fatal(c.reqPath + " " + errs.Error()) + } + req := &gpb.GetRequest{ + Prefix: prefix, + Path: func(want string) []*gpb.Path { + if want == rootPath { + return nil + } + return []*gpb.Path{&gpb.Path{Elem: resolvedPath.Elem}} + }(c.reqPath), + Encoding: gpb.Encoding_PROTO, + } + + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + resp, err := gnmiClient.Get(ctx, req) + if err != nil { + t.Fatalf("GetResponse error received from DUT (%v)", err) + } + for _, notification := range resp.GetNotification() { + if notification == nil { + t.Fatalf("GetResponse contained no Notification (%v)", prototext.Format(resp)) + } + prefixStr, err := ygot.PathToString(notification.GetPrefix()) + if err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, notification.GetPrefix()) + } + for _, update := range notification.GetUpdate() { + if update.Path == nil { + t.Fatalf("Invalid nil Path in update: %v", prototext.Format(update)) + } + elemStr, err := ygot.PathToString(update.Path) + if err != nil { + t.Fatalf("Failed to convert path to string (%v) %v", err, update.Path) + } + path := prefixStr + elemStr + + if !ignorePaths(path) { + expectedPaths[path] = operStatus{} + } + } + } + return expectedPaths +} + +// buildRequest creates a SubscribeRequest message using the specified +// parameters that include the list of paths to be added in the request. +func buildRequest(t *testing.T, params subscribeTest, target string) *gpb.SubscribeRequest { + t.Helper() + resolvedPath, errs := ygot.StringToStructuredPath(params.reqPath) + if errs != nil { + t.Fatal(params.reqPath + " " + errs.Error()) + } + + prefix := &gpb.Path{Origin: "openconfig", Target: target} + return &gpb.SubscribeRequest{ + Request: &gpb.SubscribeRequest_Subscribe{ + Subscribe: &gpb.SubscriptionList{ + Prefix: prefix, + Subscription: []*gpb.Subscription{ + &gpb.Subscription{ + Path: &gpb.Path{Elem: resolvedPath.Elem}, + Mode: params.subMode, + SampleInterval: params.sampleInterval, + SuppressRedundant: params.suppressRedundant, + HeartbeatInterval: params.heartbeatInterval, + }}, + Mode: params.mode, + Encoding: gpb.Encoding_PROTO, + UpdatesOnly: params.updatesOnly, + }, + }, + } +} diff --git a/tests/gnmi_wildcard_subscription_test.go b/tests/gnmi_wildcard_subscription_test.go new file mode 100644 index 0000000..ef12d82 --- /dev/null +++ b/tests/gnmi_wildcard_subscription_test.go @@ -0,0 +1,1292 @@ +package gnmi_wildcard_subscription_test + +import ( + "context" + "math/rand" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/openconfig/ygot/ygot" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + + gpb "github.com/openconfig/gnmi/proto/gnmi" +) + +const ( + intfPrefix = "Ethernet" + intfKey = "name" + componentKey = "name" + queueKey = "name" + intfIDKey = "interface-id" + + shortWait = 5 * time.Second + longWait = 20 * time.Second +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// Test for gNMI Subscribe for Wildcard OnChange subscriptions. +func TestWCOnChangeAdminStatus(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("0cd4c21e-0af8-41d6-b637-07b6b90ba23d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + var portList []string + ports := gnmi.GetAll(t, dut, gnmi.OC().InterfaceAny().Name().State()) + for _, port := range ports { + if strings.Contains(port, intfPrefix) { + wantUpdates[port]++ + portList = append(portList, port) + } + } + + intf, err := testhelper.RandomInterface(t, dut, &testhelper.RandomInterfaceParams{PortList: portList}) + if err != nil { + t.Fatal("No enabled interface found") + } + + state := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Enabled().State()) + predicate := oc.Interface_AdminStatus_UP + if state { + predicate = oc.Interface_AdminStatus_DOWN + } + + // Open a parallel client to watch changes to the oper-status. + // The async call needs to see the initial value be changed to + // its updated value. If this doesn't happen in a reasonable + // time (20 seconds) the test is failed. + finalValueCall := gnmi.WatchAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + AdminStatus().State(), longWait, func(val *ygnmi.Value[oc.E_Interface_AdminStatus]) bool { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + } + status, present := val.Val() + return port == intf && present && status == predicate + }) + + // Collect interfaces through subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + AdminStatus().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() && strings.Contains(port, intfPrefix) { + gotUpdates[port]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got): %v", diff) + } + + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), !state) + defer gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), state) + + _, foundUpdate := finalValueCall.Await(t) + if !foundUpdate { + t.Errorf("Interface did not receive an update for %v enabled %v", intf, !state) + } +} + +func TestWCOnChangeOperStatus(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("d0a07207-b6a2-4045-8f16-243b8ad693b6").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]bool) + ports := gnmi.GetAll(t, dut, gnmi.OC().InterfaceAny().Name().State()) + for _, port := range ports { + if strings.Contains(port, intfPrefix) { + wantUpdates[port] = true + } + } + + intf, err := testhelper.RandomInterface(t, dut, &testhelper.RandomInterfaceParams{}) + if err != nil { + t.Fatal("No enabled interface found") + } + + // Open a parallel client to watch changes to the oper-status. + // The async call needs to see the initial value be changed to + // its updated value. If this doesn't happen in a reasonable + // time (20 seconds) the test is failed. + finalValueCall := gnmi.WatchAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + OperStatus().State(), longWait, func(val *ygnmi.Value[oc.E_Interface_OperStatus]) bool { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + } + state, present := val.Val() + return port == intf && present && state == oc.Interface_OperStatus_DOWN + }) + + // Collect Interfaces through Subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + OperStatus().State(), shortWait). + Await(t) + + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), false) + defer gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), true) + + gotUpdates := make(map[string]bool) + for _, val := range initialValues { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() && strings.Contains(port, intfPrefix) { + gotUpdates[port] = true + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got): %v", diff) + } + + _, foundUpdate := finalValueCall.Await(t) + if !foundUpdate { + t.Errorf("Interface did not receive an update to %s", intf) + } +} + +func TestWCOnChangePortSpeed(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("59669bd7-e2e2-4734-869a-4bf4110b4cdc").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + paths := gnmi.CollectAll(t, dut, gnmi.OC().InterfaceAny().Ethernet(). + PortSpeed().State(), shortWait). + Await(t) + for _, path := range paths { + port, isfp, err := extractFrontPanelPortName(path.Path) + if err != nil { + t.Errorf("extractFrontPanelPortName(%v) failed: %v", path.Path, err) + continue + } + if !isfp { + continue + } + if strings.Contains(port, intfPrefix) { + wantUpdates[port]++ + } + } + + var intf string + var speed oc.E_IfEthernet_ETHERNET_SPEED + newSpeed := oc.IfEthernet_ETHERNET_SPEED_UNSET + + // Find an interface with an alternate speed + for port := range wantUpdates { + if empty, err := testhelper.TransceiverEmpty(t, dut, port); err != nil || empty { + continue + } + speed = gnmi.Get(t, dut, gnmi.OC().Interface(port).Ethernet().PortSpeed().State()) + speeds, err := testhelper.SupportedSpeedsForPort(t, dut, port) + if err != nil { + t.Logf("SupportedSpeedsForPort(%v) failed: %v", port, err) + continue + } + if len(speeds) > 1 { + intf = port + for _, s := range speeds { + if s != speed { + newSpeed = s + break + } + } + break + } + } + if newSpeed == oc.IfEthernet_ETHERNET_SPEED_UNSET { + t.Fatal("No alternate speeds found") + } + + // Open a parallel client to watch changes to the oper-status. + // The async call needs to see the initial value be changed to + // its updated value. If this doesn't happen in a reasonable + // time (20 seconds) the test is failed. + finalValueCall := gnmi.WatchAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + Ethernet(). + PortSpeed().State(), longWait, func(val *ygnmi.Value[oc.E_IfEthernet_ETHERNET_SPEED]) bool { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + } + if speed, present := val.Val(); port == intf && present && speed == newSpeed { + return true + } + return false + }) + + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + Ethernet(). + PortSpeed().State(), shortWait). + Await(t) + + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Ethernet().PortSpeed().Config(), newSpeed) + defer gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Ethernet().PortSpeed().Config(), speed) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + port, err := fetchPathKey(val.Path, "name") + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() { + if _, ok := wantUpdates[port]; !ok { + t.Errorf("Port not found in On Change update: %v", port) + } + gotUpdates[port]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got): %v", diff) + } + + _, foundUpdate := finalValueCall.Await(t) + if !foundUpdate { + t.Errorf("Interface did not receive an update for %v %v to %v", intf, speed, newSpeed) + } +} + +func fetchPathKey(path *gpb.Path, id string) (string, error) { + if path == nil { + return "", errors.New("received nil path") + } + pathStr, err := ygot.PathToString(path) + if err != nil { + return "", errors.Errorf("ygot.PathToString() failed: %v", err) + } + for _, e := range path.GetElem() { + if e.GetKey() == nil { + continue + } + if key, ok := e.GetKey()[id]; ok { + return key, nil + } + return "", errors.Errorf("failed to get key from path: %v", pathStr) + } + return "", errors.Errorf("failed to find key for path: %v", pathStr) +} + +func TestWCOnChangeId(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("f7e8ab6b-4d10-4986-811c-63044295a74d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]bool) + var portList []string + paths := gnmi.CollectAll(t, dut, gnmi.OC().InterfaceAny().Id().State(), 5*time.Second).Await(t) + for _, path := range paths { + port, isfp, err := extractFrontPanelPortName(path.Path) + if err != nil || !isfp { + continue + } + if strings.Contains(string(port), intfPrefix) { + wantUpdates[port] = true + portList = append(portList, port) + } + } + + // Randomly select one intf + intf, err := testhelper.RandomInterface(t, dut, &testhelper.RandomInterfaceParams{PortList: portList, OperDownOk: true}) + if err != nil { + t.Fatal("No interface found") + } + + res := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Config()) + idPath := gnmi.OC().Interface(intf).Id() + + var originalID uint32 = 0 + var modifiedID uint32 + var idExist bool = false + gotUpdates := make(map[string]uint32) + + if res.Id != nil { + // If the interface has already an ID set, save it so it can be restored. + originalID = *res.Id + idExist = true + } else { + rand.Seed(time.Now().Unix()) + originalID = uint32(rand.Intn(100)) + } + + modifiedID = uint32(originalID + 800) + + var wg sync.WaitGroup + wg.Add(1) + + go func(intf string) { + defer wg.Done() + // This goroutine runs ON_CHANGE subscription, wait change of id + // We are checking only on interface, so we just need the one got checked + value := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + Id().State(), 30*time.Second).Await(t) + + for _, v := range value { + // Extract front panel port. + fp, isfp, err := extractFrontPanelPortName(v.Path) + if err != nil { + t.Errorf("extractFrontPanelPortName() failed: %v", err) + continue + } + if !isfp { + continue + } + + if upd, present := v.Val(); present { + if intf == fp { + if wantUpdates[fp] { + gotUpdates[fp] = upd + } + } + } + } + }(intf) + + // Replace the originalID value with modifiedID if originalID exists before the test. + if idExist { + gnmi.Delete(t, dut, idPath.Config()) + gnmi.Update(t, dut, idPath.Config(), modifiedID) + } else { + gnmi.Update(t, dut, idPath.Config(), modifiedID) + } + time.Sleep(30 * time.Second) + wg.Wait() + + // When originalID does exist, sets the originalID back at the end of the test + defer func() { + if idExist { + gnmi.Delete(t, dut, idPath.Config()) + gnmi.Update(t, dut, idPath.Config(), originalID) + afterCall := gnmi.Watch(t, dut, gnmi.OC().Interface(intf).Id().State(), longWait, func(val *ygnmi.Value[uint32]) bool { + v, ok := val.Val() + return ok && v == originalID + }) + valueAfterCall, _ := afterCall.Await(t) + t.Logf("Modified ID got replaced back to originalID %v", valueAfterCall) + } else { + gnmi.Delete(t, dut, idPath.Config()) + } + }() + + if wantUpdates[intf] { + if modifiedID != gotUpdates[intf] { + t.Errorf("ID is updated for %v. Want to get %v, got %v ", intf, modifiedID, gotUpdates[intf]) + } + } +} + +// Returns the port name, whether its front-panel or not, and an error +func extractFrontPanelPortName(path *gpb.Path) (string, bool, error) { + if path == nil { + return "", false, errors.New("received nil path") + } + + pathStr, err := ygot.PathToString(path) + if err != nil { + return "", false, errors.Errorf("ygot.PathToString() failed: %v", err) + } + + if len(path.GetElem()) < 3 { + return "", false, errors.Errorf("No valid front panel name from path: %v", pathStr) + } + + fpEle := path.GetElem()[1] + if fpEle == nil { + return "", false, errors.Errorf("failed to get key from path: %v", pathStr) + } + + fpKey, ok := fpEle.GetKey()["name"] + if !ok { + return "", false, errors.Errorf("failed to get key from path: %v", pathStr) + } + + if !strings.Contains(fpKey, "Ethernet") { + return "", false, nil + } + + return fpKey, true, nil +} + +func TestWCOnChangeEthernetMacAddress(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c83a6e46-95a4-4dc7-a934-48c92fa0f136").Teardown(t) + t.Skip() + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + var portList []string + ports := gnmi.GetAll(t, dut, gnmi.OC().InterfaceAny().Name().State()) + for _, port := range ports { + if strings.Contains(port, intfPrefix) { + wantUpdates[port]++ + portList = append(portList, port) + } + } + + intf, err := testhelper.RandomInterface(t, dut, &testhelper.RandomInterfaceParams{PortList: portList, OperDownOk: true}) + if err != nil { + t.Fatal("No interface found") + } + + origMAC := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Ethernet().MacAddress().State()) + newMAC := "00:11:22:33:44:55" + if origMAC == newMAC { + newMAC = "55:44:33:22:11:00" + } + + // Open a parallel client to watch changes to the `mac-address`. + // The async call needs to see the initial value be changed to + // its updated value. If this doesn't happen in a reasonable + // time (20 seconds) the test is failed. + finalValueCall := gnmi.WatchAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + Ethernet(). + MacAddress().State(), longWait, func(val *ygnmi.Value[string]) bool { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + } + mac, present := val.Val() + return port == intf && present && mac == newMAC + }) + + // Collect interfaces through subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + Ethernet(). + MacAddress().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() && strings.Contains(port, intfPrefix) { + gotUpdates[port]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got): %v", diff) + } + + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Ethernet().MacAddress().Config(), newMAC) + defer gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Ethernet().MacAddress().Config(), origMAC) + + _, foundUpdate := finalValueCall.Await(t) + if !foundUpdate { + t.Errorf("Interface did not receive an update for %v `id` %s", intf, newMAC) + } +} + +func TestWCOnChangeIntegratedCircuitNodeId(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("7dd451b1-4d2b-4c79-90f5-1d419bdecc67").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every integrated circuit component through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + var icList []string + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().Name().State()) + for _, component := range components { + if component == "" { + continue + } + compTypeVal, present := testhelper.LookupComponentTypeOCCompliant(t, dut, component) + if !present || compTypeVal != "INTEGRATED_CIRCUIT" { + continue + } + wantUpdates[component]++ + icList = append(icList, component) + } + if len(icList) == 0 { + t.Fatal("No integrated circuit components found") + } + + ic := icList[len(icList)-1] + + origNodeID := gnmi.Get(t, dut, gnmi.OC().Component(ic).IntegratedCircuit().NodeId().State()) + newNodeID := origNodeID + 1 + + // Open a parallel client to watch changes to the `node-id`. + // The async call needs to see the initial value be changed to + // its updated value. If this doesn't happen in a reasonable + // time (20 seconds) the test is failed. + finalValueCall := gnmi.WatchAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + IntegratedCircuit(). + NodeId().State(), longWait, func(val *ygnmi.Value[uint64]) bool { + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + } + id, present := val.Val() + return component == ic && present && id == newNodeID + }) + + // Collect interfaces through subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + IntegratedCircuit(). + NodeId().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() { + gotUpdates[component]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got): %v", diff) + } + + gnmi.Replace(t, dut, gnmi.OC().Component(ic).IntegratedCircuit().NodeId().Config(), newNodeID) + defer gnmi.Replace(t, dut, gnmi.OC().Component(ic).IntegratedCircuit().NodeId().Config(), origNodeID) + + _, foundUpdate := finalValueCall.Await(t) + if !foundUpdate { + t.Errorf("Interface did not receive an update for %v `id` %v", ic, newNodeID) + } +} + +func TestWCOnChangeComponentOperStatus(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("794672b0-e15f-4a72-8619-f5a0bbb45e9b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every component with an oper-status through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + vals := gnmi.LookupAll(t, dut, gnmi.OC().ComponentAny().OperStatus().State()) + for _, val := range vals { + if !val.IsPresent() { + continue + } + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + wantUpdates[component]++ + } + + // Collect components through Subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + OperStatus().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + component, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() { + gotUpdates[component]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +type counterSubscription struct { + dut *ondatra.DUTDevice + t *testing.T +} + +func (c counterSubscription) subToInUnicastPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InUnicastPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToInBroadcastPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InBroadcastPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToInMulticastPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InMulticastPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToOutUnicastPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + OutUnicastPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToOutBroadcastPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + OutBroadcastPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToOutMulticastPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + OutMulticastPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToInOctets() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InOctets().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToOutOctets() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + OutOctets().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToInDiscards() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InDiscards().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToOutDiscards() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + OutDiscards().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToInErrors() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InErrors().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToOutErrors() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + OutErrors().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToInFcsErrors() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + InterfaceAny(). + Counters(). + InFcsErrors().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToTransmitPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + Qos(). + InterfaceAny(). + Output(). + QueueAny(). + TransmitPkts().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToTransmitOctets() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + Qos(). + InterfaceAny(). + Output(). + QueueAny(). + TransmitOctets().State(), longWait). + Await(c.t) +} + +func (c counterSubscription) subToDroppedPkts() []*ygnmi.Value[uint64] { + return gnmi.CollectAll(c.t, gnmiOpts(c.t, c.dut, gpb.SubscriptionMode_TARGET_DEFINED), gnmi.OC(). + Qos(). + InterfaceAny(). + Output(). + QueueAny(). + DroppedPkts().State(), longWait). + Await(c.t) +} + +type counterTest struct { + uuid string + subfun func() []*ygnmi.Value[uint64] +} + +func TestWcTargetDefinedCounters(t *testing.T) { + dut := ondatra.DUT(t, "DUT") + testCases := []struct { + name string + function func(*testing.T) + }{ + { + name: "InUnicastPkts", + function: counterTest{ + uuid: "488f9daa-9b8d-455f-b580-5dd5491d64b5", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInUnicastPkts, + }.targetDefinedCounterTest, + }, + { + name: "InBroadcastPkts", + function: counterTest{ + uuid: "edffb8d9-9040-4188-b9d3-bdb083a61f27", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInBroadcastPkts, + }.targetDefinedCounterTest, + }, + { + name: "InMulticastPkts", + function: counterTest{ + uuid: "1b20f216-1bb5-4ed6-b2ed-5a09942a2eee", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInMulticastPkts, + }.targetDefinedCounterTest, + }, + { + name: "OutUnicastPkts", + function: counterTest{ + uuid: "712042e8-b057-4f0c-bd0b-cde2861a3555", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToOutUnicastPkts, + }.targetDefinedCounterTest, + }, + { + name: "OutBroadcastPkts", + function: counterTest{ + uuid: "06cf226d-36ad-4dec-958e-89a6c2d42506", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToOutBroadcastPkts, + }.targetDefinedCounterTest, + }, + { + name: "OutMulticastPkts", + function: counterTest{ + uuid: "4b81e22d-5dd2-4ea2-95bd-25d3655978a3", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToOutMulticastPkts, + }.targetDefinedCounterTest, + }, + { + name: "InOctets", + function: counterTest{ + uuid: "30e25f1b-f79c-4824-adac-4fa6feba2f02", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInOctets, + }.targetDefinedCounterTest, + }, + { + name: "OutOctets", + function: counterTest{ + uuid: "9a55189e-c1a4-42c9-a02d-eec3d8ec5d1b", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToOutOctets, + }.targetDefinedCounterTest, + }, + { + name: "InDiscards", + function: counterTest{ + uuid: "744c4e37-e6d2-400f-999b-adb6a81b461d", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInDiscards, + }.targetDefinedCounterTest, + }, + { + name: "OutDiscards", + function: counterTest{ + uuid: "69075f31-d825-4eb2-9bbe-e9bfb95b36a6", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToOutDiscards, + }.targetDefinedCounterTest, + }, + { + name: "InErrors", + function: counterTest{ + uuid: "b416b679-f336-4d3c-9e70-cc907508cda1", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInErrors, + }.targetDefinedCounterTest, + }, + { + name: "OutErrors", + function: counterTest{ + uuid: "76b515f6-9464-42ee-aa31-2210c5c9fc29", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToOutErrors, + }.targetDefinedCounterTest, + }, + { + name: "InFcsErrors", + function: counterTest{ + uuid: "6e6aac39-eaa4-4d10-890b-aec13e981733", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToInFcsErrors, + }.targetDefinedCounterTest, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, testCase.function) + } +} + +func TestWcTargetDefinedQosCountersWc(t *testing.T) { + dut := ondatra.DUT(t, "DUT") + + testCases := []struct { + name string + function func(*testing.T) + }{ + { + name: "TransmitPkts", + function: counterTest{ + uuid: "7b1133b0-b934-4948-b087-d63108418dcb", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToTransmitPkts, + }.targetDefinedQosCounterWcTest, + }, + { + name: "TransmitOctets", + function: counterTest{ + uuid: "9c36c0ca-2551-4a7d-a7de-d47fef53d158", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToTransmitOctets, + }.targetDefinedQosCounterWcTest, + }, + { + name: "DroppedPkts", + function: counterTest{ + uuid: "88985776-2b1e-4afc-8e7b-dd1114f7070c", + subfun: counterSubscription{ + dut: dut, + t: t, + }.subToDroppedPkts, + }.targetDefinedQosCounterWcTest, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, testCase.function) + } +} + +func (c counterTest) targetDefinedCounterTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + ports := gnmi.GetAll(t, dut, gnmi.OC().InterfaceAny().Name().State()) + for _, port := range ports { + if strings.Contains(port, intfPrefix) { + wantUpdates[port] = 2 // initial subscription plus one timed update. + } + } + + // Collect interfaces through subscription to be compared to the previous GET. + subcriptionValues := c.subfun() + + gotUpdates := make(map[string]int) + for _, val := range subcriptionValues { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() && strings.Contains(port, intfPrefix) { + gotUpdates[port]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +// UMF default queue name is interface-id:queueName +// This name is used for the queue name without proper number->string mapping +func isQueueConfigured(qname, iname string) bool { + return !strings.HasPrefix(qname, iname) +} + +func (c counterTest) targetDefinedQosCounterWcTest(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(c.uuid).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]uint64) + qosInterfaces := gnmi.Get(t, dut, gnmi.OC().Qos().State()).Interface + for intf := range qosInterfaces { + queueNames := gnmi.GetAll(t, dut, gnmi.OC().Qos().Interface(intf).Output().QueueAny().Name().State()) + for _, queueName := range queueNames { + if !isQueueConfigured(queueName, intf) { + continue + } + wantUpdates[intf+","+queueName] = 2 // initial subscription plus one timed update. + } + } + + // Collect interfaces through subscription to be compared to the previous GET. + subcriptionValues := c.subfun() + + gotUpdates := make(map[string]uint64) + for _, val := range subcriptionValues { + intfQueue, err := fetchQosKey(val.Path) + if err != nil { + if !strings.Contains(err.Error(), "unconfigured") { + t.Logf("fetchQosKey() failed: %v", err) + } + continue + } + + interfaceID, queueName := intfQueue[0], intfQueue[1] + if val.IsPresent() { + gotUpdates[interfaceID+","+queueName]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +func fetchQosKey(path *gpb.Path) ([]string, error) { + if path == nil { + return nil, errors.New("received nil path") + } + pathStr, err := ygot.PathToString(path) + if err != nil { + return nil, errors.Errorf("ygot.PathToString() failed: %v", err) + } + + if len(path.GetElem()) != 8 { + return nil, errors.Errorf("no valid interface id or queue name from path: %v", pathStr) + } + + interfaceEle := path.GetElem()[2].GetKey() + if interfaceEle == nil { + return nil, errors.Errorf("no valid interface id from path: %v", pathStr) + } + interfaceKey, ok := interfaceEle[intfIDKey] + if !ok { + return nil, errors.Errorf("no valid interface id from path: %v", pathStr) + } + + queueEle := path.GetElem()[5].GetKey() + if queueEle == nil { + return nil, errors.Errorf("no valid queue name from path: %v", pathStr) + } + queueName, ok := queueEle[queueKey] + if !ok { + return nil, errors.Errorf("no valid queue name from path: %v", pathStr) + } + if !isQueueConfigured(queueName, interfaceKey) { + return nil, errors.Errorf("unconfigured queue found: %v", pathStr) + } + + return []string{interfaceKey, queueName}, nil +} + +func TestWCOnChangeSoftwareModuleModuleType(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("ff05e9c6-b57b-4128-9535-e8543dc5aedc").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every software module component through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().Name().State()) + for _, component := range components { + if component == "" { + continue + } + compTypeVal, present := testhelper.LookupComponentTypeOCCompliant(t, dut, component) + if !present || compTypeVal != "SOFTWARE_MODULE" { + continue + } + wantUpdates[component]++ + } + if len(wantUpdates) == 0 { + t.Fatal("No software module components found") + } + + // Collect interfaces through subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + SoftwareModule(). + ModuleType().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() { + gotUpdates[component]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +func TestWCOnChangeHardwarePort(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("acfc84d1-b76f-45b3-bb8f-267abca3b2d2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Collect every interface through a GET to be compared to the SUBSCRIBE. + wantUpdates := make(map[string]int) + ports := gnmi.GetAll(t, dut, gnmi.OC().InterfaceAny().Name().State()) + for _, port := range ports { + if strings.Contains(port, intfPrefix) { + wantUpdates[port]++ + } + } + + // Collect interfaces through subscription to be compared to the previous GET. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + InterfaceAny(). + HardwarePort().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + port, err := fetchPathKey(val.Path, intfKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() && strings.Contains(port, intfPrefix) { + gotUpdates[port]++ + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got): %v", diff) + } +} + +func TestWCOnChangeComponentType(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("e07ea34c-b217-4aef-99ac-2516b0b5c393").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Determine that updates are received from all expected components. + wantUpdates := make(map[string]int) + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) + for _, component := range components { + if component.GetName() == "" { + continue + } + + if _, present := testhelper.LookupComponentTypeOCCompliant(t, dut, component.GetName()); !present { + continue + } + + wantUpdates[component.GetName()]++ + } + + // Collect components updates from ON_CHANGE subscription. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + Type().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]int) + for _, val := range initialValues { + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if val.IsPresent() { + gotUpdates[component] = 1 + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +func TestWCOnChangeComponentParent(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("9c889baf-c3c2-4ce3-bb74-36b78c5b77ca").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Determine expected components. + wantUpdates := make(map[string]string) + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) + for _, component := range components { + if component == nil || component.Name == nil || component.GetName() == "" { + continue + } + if component.Parent != nil && component.GetParent() != "" { + wantUpdates[component.GetName()] = component.GetParent() + } + } + + // Collect component updates from ON_CHANGE subscription. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + Parent().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]string) + for _, val := range initialValues { + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if upd, present := val.Val(); present && upd != "" { + gotUpdates[component] = upd + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +func TestWCOnChangeComponentSoftwareVersion(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("25e36fae-82e7-4d51-8f60-df6fb139f6ca").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Determine expected components. + wantUpdates := make(map[string]string) + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) + for _, component := range components { + if component == nil || component.Name == nil || component.GetName() == "" { + continue + } + if component.SoftwareVersion != nil && component.GetSoftwareVersion() != "" { + wantUpdates[component.GetName()] = component.GetSoftwareVersion() + } + } + + // Collect component updates from ON_CHANGE subscription. + initialValues := gnmi.CollectAll(t, gnmiOpts(t, dut, gpb.SubscriptionMode_ON_CHANGE), gnmi.OC(). + ComponentAny(). + SoftwareVersion().State(), shortWait). + Await(t) + + gotUpdates := make(map[string]string) + for _, val := range initialValues { + component, err := fetchPathKey(val.Path, componentKey) + if err != nil { + t.Errorf("fetchPathKey() failed: %v", err) + continue + } + if upd, present := val.Val(); present && upd != "" { + gotUpdates[component] = upd + } + } + + if diff := cmp.Diff(wantUpdates, gotUpdates); diff != "" { + t.Errorf("Update notifications comparison failed! (-want +got):\n%v", diff) + } +} + +func gnmiOpts(t *testing.T, dut *ondatra.DUTDevice, mode gpb.SubscriptionMode) *gnmi.Opts { + client, err := dut.RawAPIs().BindingDUT().DialGNMI(context.Background()) + if err != nil { + t.Fatalf("DialGNMI() failed: %v", err) + } + return dut.GNMIOpts(). + WithClient(client). + WithYGNMIOpts(ygnmi.WithSubscriptionMode(mode)) +} diff --git a/tests/gnoi_file_test.go b/tests/gnoi_file_test.go new file mode 100644 index 0000000..9ae2a48 --- /dev/null +++ b/tests/gnoi_file_test.go @@ -0,0 +1,69 @@ +package gnoi_file_test + +// This suite of tests is to end-to-end test the gNOI File service. These tests are PINs specific +// and depend on the files that are permitted to be modiified. + +import ( + "context" + "fmt" + "testing" + + "github.com/openconfig/ondatra" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + + filepb "github.com/openconfig/gnoi/file" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func TestGnoiFileRemoveWrongFile(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("841160cb-9ac7-4084-ac8b-f68d6b4c668c").Teardown(t) + dut := ondatra.DUT(t, "DUT") + t.Logf("DUT name: %v", dut.Name()) + + filename := "/tmp/foobar.txt" + if out, err := testhelper.RunSSH(dut.Name(), "touch "+filename); err != nil { + t.Fatalf("Failed to create dummy file %s: err=%v, out=%s", filename, err, out) + } + defer func() { + if out, err := testhelper.RunSSH(dut.Name(), "rm -f "+filename); err != nil { + t.Errorf("Failed to remove dummy file %s: err=%v, output=%s", filename, err, out) + } + }() + + req := &filepb.RemoveRequest{ + RemoteFile: filename, + } + // Removing an unsupported file should fail. + if _, err := dut.RawAPIs().GNOI(t).File().Remove(context.Background(), req); err == nil { + t.Errorf("Removing %s unexpectedly succeeded.", filename) + } +} + +func TestGnoiFileRemoveSucceeds(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("ddbe7f4f-33c7-4fca-944b-48c2ccddb270").Teardown(t) + dut := ondatra.DUT(t, "DUT") + t.Logf("DUT name: %v", dut.Name()) + + filename := "/mnt/region_config/container_files/etc/sonic/config_db.json" + backup := "/tmp/config_db.json.bak" + + if out, err := testhelper.RunSSH(dut.Name(), fmt.Sprintf("cp %s %s", filename, backup)); err != nil { + t.Fatalf("Failed to copy file %s to %s: err=%v, out=%s", filename, backup, err, out) + } + defer func() { + if out, err := testhelper.RunSSH(dut.Name(), fmt.Sprintf("mv %s %s", backup, filename)); err != nil { + t.Errorf("Failed to restore backup file %s to %s: err=%v, output=%s", backup, filename, err, out) + } + }() + + req := &filepb.RemoveRequest{ + RemoteFile: filename, + } + if _, err := dut.RawAPIs().GNOI(t).File().Remove(context.Background(), req); err != nil { + t.Errorf("Error removing %s: %v", filename, err) + } +} diff --git a/tests/gnoi_reboot_test.go b/tests/gnoi_reboot_test.go new file mode 100644 index 0000000..d500ac8 --- /dev/null +++ b/tests/gnoi_reboot_test.go @@ -0,0 +1,470 @@ +package gnoi_reboot_test + +// This suite of tests is to end-to-end test the gNOI reboot. + +import ( + "context" + "regexp" + "strings" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ygnmi/ygnmi" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + syspb "github.com/openconfig/gnoi/system" +) + +type grpcErr struct { + code codes.Code + desc string +} + +func extractCanonicalCodeString(e error) string { + if e == nil { + return codes.OK.String() + } + re := regexp.MustCompile(`code = (\w+)`) + match := re.FindStringSubmatch(e.Error()) + if len(match) < 2 { + return codes.Unknown.String() + } + return match[1] +} + +func (e *grpcErr) Error() string { + if e == nil { + return "" + } + return "rpc error: code = " + e.code.String() + " desc = " + e.desc +} + +func (e *grpcErr) Is(o error) bool { + if e == nil || o == nil { + return e == o + } + + if extractCanonicalCodeString(o) != e.code.String() { + return false + } + if !strings.Contains(o.Error(), e.desc) { + return false + } + return true +} + +var ( + errStrRebootRPC = "reboot RPC failed: rpc error: " + errInvalidRequest = grpcErr{code: codes.InvalidArgument, desc: "Invalid request"} + errHostService = grpcErr{code: codes.Internal, desc: "Internal SONiC HostService failure: "} + errMethodNotSupported = grpcErr{code: codes.InvalidArgument, desc: "reboot method is not supported"} +) + +// attainGnoiStateParams specify the parameters used by attainGnoiStateDuringReboot. +type attainGnoiStateParams struct { + waitTime time.Duration + checkInterval time.Duration + timeBeforeReboot int64 + gnoiReachability bool +} + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// Helper function to create the Get Request. +func createGetRequest(dut *ondatra.DUTDevice, paths []*gpb.Path, dataType gpb.GetRequest_DataType) *gpb.GetRequest { + // Add Prefix information for the GetRequest. + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + // Create getRequest message with data type. + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: paths, + Type: dataType, + Encoding: gpb.Encoding_PROTO, + } + return getRequest +} + +// Helper function to get bootTime from chassis using a raw gNMI client: the intention is to not fatally +// fail is the chassis is unreachable. +// Return boot-time, true, nil if boot-time successfully retrieved from the switch. +// Return 0, false, nil if unable to retrieve boot-time. +// Return 0, false, error if an error occurred while retrieving boot-time. +func retrieveBootTime(t *testing.T, d *ondatra.DUTDevice) (uint64, bool, error) { + operStatusPath := gnmi.OC().System().BootTime().State().PathStruct() + resolvedPath, _, _ := ygnmi.ResolvePath(operStatusPath) + paths := []*gpb.Path{resolvedPath} + getRequest := createGetRequest(d, paths, gpb.GetRequest_STATE) + ctx := context.Background() + gnmiClient, err := d.RawAPIs().BindingDUT().DialGNMI(ctx, grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + getResp, err := gnmiClient.Get(ctx, getRequest) + + if err != nil { + // The inability to retrieve boot-time is not a fatal error and is an expected side effect + // of rebooting during reboot test. + t.Logf("error retrieving boot-time, err = (%v)", err) + return 0, false, nil + } + if getResp == nil { + return 0, false, errors.Errorf("retrieveBootTime: Get response is nil") + } + + notifs := getResp.GetNotification() + if len(notifs) != 1 { + return 0, false, errors.Errorf("retrieveBootTime: There should only be one notification: getResp = (%v)", getResp) + } + + update := notifs[0].GetUpdate() + if len(update) != 1 { + return 0, false, errors.Errorf("retrieveBootTime: There should only be one update: getResp = (%v)", getResp) + } + + bootTime := update[0].GetVal() + if bootTime == nil { + return 0, false, errors.Errorf("retrieveBootTime: Unable to GetVal from update: getResp = (%v)", getResp) + } + return bootTime.GetUintVal(), true, nil +} + +// attainGnoiStateDuringReboot polls the gNOI server and achieve the corresponding gNOI state depending on gnoiReachability during reboot +// If gnoiReachability is true, then this function will poll for gNOI server to be reachable and verifies the reboot. +// If gnoiReachability is false, then this function will poll for gNOI server to be unreachable and returns. +func attainGnoiStateDuringReboot(t *testing.T, d *ondatra.DUTDevice, params attainGnoiStateParams) error { + t.Helper() + t.Logf("Polling gNOI server reachability in %v intervals for max duration of %v", params.checkInterval, params.waitTime) + for timeout := time.Now().Add(params.waitTime); time.Now().Before(timeout); { + // The switch backend might not have processed the reboot request or might take + // sometime to execute the request. So wait for check interval time and + // later verify that the switch rebooted within the specified wait time. + time.Sleep(params.checkInterval) + timeElapsed := (time.Now().UnixNano() - params.timeBeforeReboot) / int64(time.Second) + + // An error returned by GNOIAble indicates we were unable to connect to the server. + gnoiErr := testhelper.GNOIAble(t, d) + + // Treat a non GNOIAble switch and an inability to query boot-time as a server not up condition. + if gnoiErr != nil { + t.Logf("gNOI server not up after %v seconds", timeElapsed) + if !params.gnoiReachability { + return nil + } + continue + } + + // An error returned by retrieveBootTime is a processing error to be treated as fatal. + bootTime, valid, bootErr := retrieveBootTime(t, d) + + if bootErr != nil { + return bootErr + } + + if !valid { + t.Logf("gNOI server not up after %v seconds", timeElapsed) + if !params.gnoiReachability { + return nil + } + continue + } + + t.Logf("gNOI server up after %v seconds", timeElapsed) + + // An extra check to ensure that the system has rebooted. + if bootTime < uint64(params.timeBeforeReboot) { + t.Logf("Switch has not rebooted after %v seconds", timeElapsed) + continue + } + + t.Logf("Switch rebooted after %v seconds", timeElapsed) + if !params.gnoiReachability { + return errors.Errorf("failed to reach gNOI unreachability") + } + return nil + } + return errors.Errorf("failed to reboot") +} + +func TestRebootSuccess(t *testing.T) { + // Validation for success of reboot after sending Reboot RPC. + ttID := "0dedda87-1b76-40a2-8712-24c1579987ee" + defer testhelper.NewTearDownOptions(t).WithID(ttID).Teardown(t) + dut := ondatra.DUT(t, "DUT") + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30*time.Second).WithRequest(syspb.RebootMethod_COLD).WithLatencyMeasurement(ttID, "gNOI Reboot With Type: "+syspb.RebootMethod_COLD.String()) + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Failed to reboot DUT: %v", err) + } +} + +func TestRebootStatus(t *testing.T) { + // Verify RebootStatus when there is no active reboot. + defer testhelper.NewTearDownOptions(t).WithID("dcc5d482-9417-42a5-9801-b51cbf7c9ff3").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + req := &syspb.RebootStatusRequest{} + + resp, err := dut.RawAPIs().GNOI(t).System().RebootStatus(context.Background(), req) + if err != nil { + t.Fatalf("Failed to send RebootStatus RPC: %v", err) + } + + if got, want := resp.GetActive(), false; got != want { + t.Errorf("RebootStatus(whenInactiveReboot).active = %v, want:%v", got, want) + } + if got, want := resp.GetWhen(), uint64(0); got != want { + t.Errorf("RebootStatus(whenInactiveReboot).when = %v, want:%v", got, want) + } + if got, want := resp.GetReason(), ""; got != want { + t.Errorf("RebootStatus(whenInactiveReboot).reason = %v, want:%v", got, want) + } +} + +func TestCancelRebootNotSupported(t *testing.T) { + // This test is Google specific as other vendors might support CancelReboot. + // Validate that CancelReboot RPC is not supported. + defer testhelper.NewTearDownOptions(t).WithID("54890e78-97c2-4c08-b03c-0822870691e7").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + req := &syspb.CancelRebootRequest{ + Message: "Test message to cancel reboot", + } + + wantError := grpcErr{code: codes.Unimplemented, desc: "Method System.CancelReboot is unimplemented"} + if _, err := dut.RawAPIs().GNOI(t).System().CancelReboot(context.Background(), req); !wantError.Is(err) { + t.Errorf("Failed to validate that CancelReboot is not supported: %v", err) + } +} + +func TestScheduledRebootNotSupported(t *testing.T) { + // Validate that scheduled Reboot RPC is not supported. + defer testhelper.NewTearDownOptions(t).WithID("9d5c0ded-7474-47cf-8310-9444189928cd").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + + req := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Delay: 10, // in nanoseconds + Message: "Test Delayed Reboot", + } + + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30 * time.Second).WithRequest(req) + if err := testhelper.Reboot(t, dut, params); err == nil || extractCanonicalCodeString(err) != codes.InvalidArgument.String() { + t.Errorf("Failed to validate that delayed reboot is not supported: %v", err) + } +} + +func TestRebootMethodsValidation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c416cba7-12f0-4efa-a341-0c5d1c806fc1").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + + tests := []struct { + method syspb.RebootMethod + wantErr grpcErr + }{ + { + method: syspb.RebootMethod_UNKNOWN, + wantErr: errMethodNotSupported, + }, + { + method: syspb.RebootMethod_HALT, + wantErr: errMethodNotSupported, + }, + { + method: syspb.RebootMethod_WARM, + wantErr: grpcErr{code: errHostService.code, desc: errHostService.desc + "Warm reboot is currently not supported."}, + }, + { + method: syspb.RebootMethod_NSF, + wantErr: errMethodNotSupported, + }, + { + method: syspb.RebootMethod_POWERUP, + wantErr: errMethodNotSupported, + }, + { + method: syspb.RebootMethod_POWERDOWN, + wantErr: grpcErr{code: errHostService.code, desc: errHostService.desc + "Invalid reboot method: 2"}, + }, + } + + for _, tt := range tests { + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30 * time.Second).WithRequest(tt.method) + if err := testhelper.Reboot(t, dut, params); !tt.wantErr.Is(err) { + t.Errorf("Failed to validate that %v reboot method is not supported: %v", tt.method, err) + } + } +} + +func TestRebootStatusWhenActiveReboot(t *testing.T) { + // Verify RebootStatus response when there is an active reboot. + defer testhelper.NewTearDownOptions(t).WithID("23bbc091-ba7f-4424-9db6-fe5e25274791").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + + rebootStatusReq := &syspb.RebootStatusRequest{} + + rebootReqMessage := "Test message to issue reboot." + rebootReq := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: rebootReqMessage, + } + + // Issue a reboot. + timeBeforeReboot := time.Now().UnixNano() + systemClient := dut.RawAPIs().GNOI(t).System() + if _, err := systemClient.Reboot(context.Background(), rebootReq); err != nil { + t.Fatalf("Failed to issue Reboot: %v", err) + } + + // Retrieve RebootStatus immediately after issuing reboot and verify the response. + resp, err := systemClient.RebootStatus(context.Background(), rebootStatusReq) + if err != nil { + t.Errorf("Failed to get RebootStatus: %v", err) + } else { + if got, want := resp.GetActive(), true; got != want { + t.Errorf("RebootStatus(whenActiveReboot).active = %v, want:%v", got, want) + } + if got, wantMin, wantMax := resp.GetWhen(), uint64(timeBeforeReboot), uint64(time.Now().UnixNano()); got >= wantMin && got <= wantMax { + t.Errorf("RebootStatus(whenActiveReboot).when = %v, wantMin:%v, wantMax:%v", got, wantMin, wantMax) + } + if got, want := resp.GetReason(), rebootReqMessage; got != want { + t.Errorf("RebootStatus(whenActiveReboot).reason = %v, want:%v", got, want) + } + } + + params := attainGnoiStateParams{ + waitTime: waitTime, + checkInterval: 30 * time.Second, + timeBeforeReboot: timeBeforeReboot, + gnoiReachability: true, + } + if err := attainGnoiStateDuringReboot(t, dut, params); err != nil { + t.Errorf("Failed to poll gNOI reachability and verify reboot: %v", err) + } +} + +func TestRebootRequestWhenActiveReboot(t *testing.T) { + // Verify that new Reboot request will be rejected during an active reboot. + defer testhelper.NewTearDownOptions(t).WithID("e399daad-f61e-4918-a02c-1802f27de983").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + + firstRebootReq := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: "First test message to issue reboot.", + } + + SecondRebootReq := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: "Second test message to issue reboot.", + } + + timeBeforeReboot := time.Now().UnixNano() + systemClient := dut.RawAPIs().GNOI(t).System() + + // Issue first reboot. + if _, err := systemClient.Reboot(context.Background(), firstRebootReq); err != nil { + t.Fatalf("Failed to issue Reboot: %v", err) + } + + wantErr := grpcErr{code: errHostService.code, desc: errHostService.desc + "Previous reboot is ongoing"} + // Issue another reboot immediately after issuing the first reboot and verify that the second reboot got rejected. + if _, err := systemClient.Reboot(context.Background(), SecondRebootReq); !wantErr.Is(err) { + t.Errorf("Failed to validate that the switch rejects second reboot: %v", err) + } + + params := attainGnoiStateParams{ + waitTime: waitTime, + checkInterval: 30 * time.Second, + timeBeforeReboot: timeBeforeReboot, + gnoiReachability: true, + } + if err := attainGnoiStateDuringReboot(t, dut, params); err != nil { + t.Errorf("Failed to poll gNOI reachability and verify reboot: %v", err) + } + +} + +func TestRebootRequestWhenGnoiUnreachable(t *testing.T) { + // Verify Reboot request will be rejected if issued when gNOI is unreachable. + defer testhelper.NewTearDownOptions(t).WithID("bfbe4e85-4559-4184-acf0-b00bb4bf46ba").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + + firstRebootReq := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: "First test message to issue reboot.", + } + + SecondRebootReq := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: "Second test message to issue reboot.", + } + + timeBeforeReboot := time.Now().UnixNano() + systemClient := dut.RawAPIs().GNOI(t).System() + + // Issue first reboot. + if _, err := systemClient.Reboot(context.Background(), firstRebootReq); err != nil { + t.Fatalf("Failed to issue Reboot: %v", err) + } + + // Poll gNOI until it is unreachable and issue a second reboot. + params := attainGnoiStateParams{ + waitTime: waitTime, + checkInterval: 15 * time.Second, + timeBeforeReboot: timeBeforeReboot, + gnoiReachability: false, + } + if err := attainGnoiStateDuringReboot(t, dut, params); err != nil { + t.Fatalf("Failed to reach a state where GNOI is unreachable: %v", err) + } + + wantErr := grpcErr{code: codes.Unavailable} + // Issue second reboot while gNOI is unreachable and verify it's rejection. + if _, err := systemClient.Reboot(context.Background(), SecondRebootReq); !wantErr.Is(err) { + t.Errorf("Failed to validate that the switch rejects reboot when gNOI is unreachable: %v", err) + } + + params.checkInterval = 30 * time.Second + params.gnoiReachability = true + if err := attainGnoiStateDuringReboot(t, dut, params); err != nil { + t.Errorf("Failed to poll gNOI reachability and verify reboot: %v", err) + } + +} diff --git a/tests/inband_sw_interface_dual_switch_test.go b/tests/inband_sw_interface_dual_switch_test.go new file mode 100644 index 0000000..c15c098 --- /dev/null +++ b/tests/inband_sw_interface_dual_switch_test.go @@ -0,0 +1,355 @@ +package inband_sw_interface_dual_switch_test + +import ( + "net" + "testing" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +// These are the counters we track in these tests. +type counters struct { + inPkts uint64 + outPkts uint64 + inOctets uint64 + outOctets uint64 +} + +var ( + inbandSwIntfName = "Loopback0" + interfaceIndex = uint32(0) + configuredIPv4Path = "10.10.10.10" + configuredIPv4PrefixLength = uint8(32) + configuredIPv6Path = "3000::2" + configuredIPv6PrefixLength = uint8(128) + calledMockConfigPush = false +) + +// readCounters reads all the counters via GNMI and returns a counters struct. +func readCounters(t *testing.T, dut *ondatra.DUTDevice, intf string) *counters { + t.Helper() + cntStruct := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().State()) + return &counters{ + inPkts: cntStruct.GetInPkts(), + outPkts: cntStruct.GetOutPkts(), + inOctets: cntStruct.GetInOctets(), + outOctets: cntStruct.GetOutOctets(), + } +} + +// showCountersDelta shows debug info after an unexpected change in counters. +func showCountersDelta(t *testing.T, before *counters, after *counters, expect *counters) { + t.Helper() + + for _, s := range []struct { + desc string + before, after, expect uint64 + }{ + {"in-pkts", before.inPkts, after.inPkts, expect.inPkts}, + {"out-pkts", before.outPkts, after.outPkts, expect.outPkts}, + {"in-octets", before.inOctets, after.inOctets, expect.inOctets}, + {"out-octets", before.outOctets, after.outOctets, expect.outOctets}, + } { + if s.before != s.after || s.expect != s.before { + t.Logf("%v %d -> %d expected %d (%+d)", s.desc, s.before, s.after, s.expect, s.after-s.before) + } + } +} + +func mockConfigPush(t *testing.T) { + // Performs a mock config push by setting up the loopback0 interface database + // entries and the IPv4 and IPv6 addresses expected to be configured. + // TODO: Remove calls to this function once the helper function + // to perform a default config during setup is available. See b/188927677. + + if calledMockConfigPush { + return + } + + // Create the loopback0 interface. + t.Logf("Config push for %v", inbandSwIntfName) + dut := ondatra.DUT(t, "DUT") + d := &oc.Root{} + + newIface := d.GetOrCreateInterface(inbandSwIntfName) + newIface.Name = &inbandSwIntfName + newIface.Type = oc.IETFInterfaces_InterfaceType_softwareLoopback + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Config(), newIface) + + iface := d.GetOrCreateInterface(inbandSwIntfName).GetOrCreateSubinterface(interfaceIndex) + + // Seed an IPv4 address for the loopback0 interface. + t.Logf("Config push for %v/%v", configuredIPv4Path, configuredIPv4PrefixLength) + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(configuredIPv4Path) + newV4.Ip = &configuredIPv4Path + newV4.PrefixLength = &configuredIPv4PrefixLength + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(configuredIPv4Path).Config(), newV4) + + gnmi.Await(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(configuredIPv4Path).Ip().State(), 5*time.Second, configuredIPv4Path) + + // Seed an IPv6 address for the loopback0 interface. + t.Logf("Config push for %v/%v", configuredIPv6Path, configuredIPv6PrefixLength) + newV6 := iface.GetOrCreateIpv6().GetOrCreateAddress(configuredIPv6Path) + newV6.Ip = &configuredIPv6Path + newV6.PrefixLength = &configuredIPv6PrefixLength + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Path).Config(), newV6) + + gnmi.Await(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Path).Ip().State(), 5*time.Second, configuredIPv6Path) + + calledMockConfigPush = true +} + +// Tests start here. +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// TestGNMIInbandSwLoopbackInCnts - Check Loopback0 in-traffic counters +func TestGNMIInbandSwLoopbackInCnts(t *testing.T) { + const ( + pktsPerTry uint64 = 50 + counterUpdateDelay = 1500 * time.Millisecond + packetPayloadSize = 1000 + ) + + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("8e6b32f4-cf39-419f-ba36-db9c778ad317").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + control := ondatra.DUT(t, "CONTROL") + mockConfigPush(t) + + // Select a random front panel interface EthernetX. + params := testhelper.RandomInterfaceParams{ + PortList: []string{ + dut.Port(t, "port1").Name(), + dut.Port(t, "port2").Name(), + dut.Port(t, "port3").Name(), + dut.Port(t, "port4").Name(), + }} + intf, err := testhelper.RandomInterface(t, dut, ¶ms) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + bad := false + i := 0 + + // Iterate up to 5 times to get a successful test. + for i = 1; i <= 5; i++ { + t.Logf("\n----- TestGNMIInbandSwLoopbackInCnts: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := readCounters(t, dut, inbandSwIntfName) + + // Construct packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, + DstMAC: net.HardwareAddr{0x00, 0x1a, 0x11, 0x17, 0x5f, 0x80}, + EthernetType: layers.EthernetTypeIPv4, + } + ip := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolTCP, + SrcIP: net.ParseIP("10.10.20.30").To4(), + DstIP: net.ParseIP(configuredIPv4Path).To4(), + } + tcp := &layers.TCP{ + SrcPort: 10000, + DstPort: 22, + Seq: 11050, + } + // Required for checksum computation. + tcp.SetNetworkLayerForChecksum(ip) + + data := make([]byte, packetPayloadSize) + for i := range data { + data[i] = 0xfe + } + payload := gopacket.Payload(data) + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields based on packet headers. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, eth, ip, tcp, payload); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + // Compute the expected counters after the test. + expect := before + // Currently, counter increasing is not supported on loopback (b/197764888) + // Uncomment below 2 lines when it becomes supported. + // expect.inPkts += pktsPerTry + // expect.inOctets += pktsPerTry * uint64(len(buf.Bytes())) + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, // or "Ethernet8" for testing + Count: uint(pktsPerTry), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, control, control.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. At 500ms we frequently + // read the counters before they're updated. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + if after := readCounters(t, dut, inbandSwIntfName); *after != *expect { + showCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIInbandSwLoopbackInCnts: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIInbandSwLoopbackInCnts: SUCCESS after %v Iteration(s) -----\n\n", i) +} + +// TestGNMIInbandSwLoopbackOutCnts - Check Loopback0 out-traffic counters +func TestGNMIInbandSwLoopbackOutCnts(t *testing.T) { + const ( + pktsPerTry uint64 = 50 + counterUpdateDelay = 1500 * time.Millisecond + packetPayloadSize = 1000 + ) + + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("57fbd43d-eeb3-478d-9740-69d9bb23fca6").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + // Select a random front panel interface EthernetX. + params := testhelper.RandomInterfaceParams{ + PortList: []string{ + dut.Port(t, "port1").Name(), + dut.Port(t, "port2").Name(), + dut.Port(t, "port3").Name(), + dut.Port(t, "port4").Name(), + }} + intf, err := testhelper.RandomInterface(t, dut, ¶ms) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + bad := false + i := 0 + + // Iterate up to 5 times to get a successful test. + for i = 1; i <= 5; i++ { + t.Logf("\n----- TestGNMIInbandSwLoopbackOutCnts: Iteration %v -----\n", i) + bad = false + + // Read all the relevant counters initial values. + before := readCounters(t, dut, inbandSwIntfName) + + // Construct packet. + eth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x66, 0x77, 0x88}, + DstMAC: net.HardwareAddr{0x00, 0x1a, 0x11, 0x17, 0x5f, 0x80}, + EthernetType: layers.EthernetTypeIPv4, + } + ip := &layers.IPv4{ + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolTCP, + SrcIP: net.ParseIP(configuredIPv4Path).To4(), + DstIP: net.ParseIP("10.10.20.30").To4(), + } + tcp := &layers.TCP{ + SrcPort: 10000, + DstPort: 22, + Seq: 11050, + } + // Required for checksum computation. + tcp.SetNetworkLayerForChecksum(ip) + + data := make([]byte, packetPayloadSize) + for i := range data { + data[i] = 0xfe + } + payload := gopacket.Payload(data) + + buf := gopacket.NewSerializeBuffer() + + // Enable reconstruction of length and checksum fields based on packet headers. + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, eth, ip, tcp, payload); err != nil { + t.Fatalf("Failed to serialize packet (%v)", err) + } + + // Compute the expected counters after the test. + expect := before + // Currently, counter increasing is not supported on loopback (b/197764888) + // Uncomment below 2 lines when it becomes supported. + // expect.inPkts += pktsPerTry + // expect.inOctets += pktsPerTry * uint64(len(buf.Bytes())) + + packetOut := &testhelper.PacketOut{ + EgressPort: intf, // or "Ethernet8" for testing + Count: uint(pktsPerTry), + Interval: 1 * time.Millisecond, + Packet: buf.Bytes(), + } + + p4rtClient, err := testhelper.FetchP4RTClient(t, dut, dut.RawAPIs().P4RT(t), nil) + if err != nil { + t.Fatalf("Failed to create P4RT client: %v", err) + } + if err := p4rtClient.SendPacketOut(t, packetOut); err != nil { + t.Fatalf("SendPacketOut operation failed for %+v (%v)", packetOut, err) + } + + // Sleep for enough time that the counters are polled after the + // transmit completes sending bytes. At 500ms we frequently + // read the counters before they're updated. + time.Sleep(counterUpdateDelay) + + // Read all the relevant counters again. + if after := readCounters(t, dut, inbandSwIntfName); *after != *expect { + showCountersDelta(t, before, after, expect) + bad = true + } + + if !bad { + break + } + } + + if bad { + t.Fatalf("\n\n----- TestGNMIInbandSwLoopbackOutCnts: FAILED after %v Iterations -----\n\n", i-1) + } + + t.Logf("\n\n----- TestGNMIInbandSwLoopbackOutCnts: SUCCESS after %v Iteration(s) -----\n\n", i) +} diff --git a/tests/inband_sw_interface_test.go b/tests/inband_sw_interface_test.go new file mode 100644 index 0000000..edf21c1 --- /dev/null +++ b/tests/inband_sw_interface_test.go @@ -0,0 +1,273 @@ +package inband_sw_interface_test + +import ( + "net" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/testt" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +var ( + inbandSwIntfName = "Loopback0" + interfaceIndex = uint32(0) + configuredIPv4Path = "10.10.40.20" + configuredIPv4PrefixLength = uint8(32) + configuredIPv6Path = "3000::2" + configuredIPv6PrefixLength = uint8(128) + newConfiguredIPv4Path = "10.10.50.15" + newConfiguredIPv4PrefixLength = uint8(32) + newConfiguredIPv6Path = "3022::2345" + newConfiguredIPv6PrefixLength = uint8(128) +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +var calledMockConfigPush = false + +func mockConfigPush(t *testing.T) { + // Performs a mock config push by setting up the loopback0 interface database + // entries and the IPv4 and IPv6 addresses expected to be configured. + // TODO: Remove calls to this function once the helper function + // to perform a default config during setup is available. See b/188927677. + + if calledMockConfigPush { + return + } + + // Create the loopback0 interface. + t.Logf("Config push for %v", inbandSwIntfName) + dut := ondatra.DUT(t, "DUT") + d := &oc.Root{} + + newIface := d.GetOrCreateInterface(inbandSwIntfName) + newIface.Name = &inbandSwIntfName + newIface.Type = oc.IETFInterfaces_InterfaceType_softwareLoopback + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Config(), newIface) + + iface := d.GetOrCreateInterface(inbandSwIntfName).GetOrCreateSubinterface(interfaceIndex) + + // Seed an IPv4 address for the loopback0 interface. + t.Logf("Config push for %v/%v", configuredIPv4Path, configuredIPv4PrefixLength) + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(configuredIPv4Path) + newV4.Ip = &configuredIPv4Path + newV4.PrefixLength = &configuredIPv4PrefixLength + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(configuredIPv4Path).Config(), newV4) + + gnmi.Await(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(configuredIPv4Path).Ip().State(), 5*time.Second, configuredIPv4Path) + + // Seed an IPv6 address for the loopback0 interface. + t.Logf("Config push for %v/%v", configuredIPv6Path, configuredIPv6PrefixLength) + newV6 := iface.GetOrCreateIpv6().GetOrCreateAddress(configuredIPv6Path) + newV6.Ip = &configuredIPv6Path + newV6.PrefixLength = &configuredIPv6PrefixLength + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Path).Config(), newV6) + + gnmi.Await(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Path).Ip().State(), 5*time.Second, configuredIPv6Path) + + calledMockConfigPush = true +} + +// TestGNMIInbandSwIntfName - Check inband sw interface name is expected value. +func TestGNMIInbandSwIntfName(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("c147f71d-cd60-4a14-b168-1e50c3003a1d").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + if stateName := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Name().State()); stateName != inbandSwIntfName { + t.Errorf("Inband sw interface state Name is %v, wanted %v", stateName, inbandSwIntfName) + } + + if configName := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Name().Config()); configName != inbandSwIntfName { + t.Errorf("Inband sw interface config Name is %v, wanted %v", configName, inbandSwIntfName) + } +} + +// TestGNMIInbandSwIntfType - Check inband sw interface type is expected value. +func TestGNMIInbandSwIntfType(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("6b4a4bba-b102-4706-ae11-bfb3b0b35cde").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + if stateType := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Type().State()); stateType != oc.IETFInterfaces_InterfaceType_softwareLoopback { + t.Errorf("Inband sw interface state Type is %v, wanted %v", stateType, "software loopback") + } +} + +// TestGNMIInbandSwIntfMacAddr - Check inband sw interface MAC address is expected format +// TODO: remove this comment: Currently this test fails due to b/192485691. +func TestGNMIInbandSwIntfMacAddr(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("f43768b1-7347-4c25-9f8a-c4c94d0ec923").Teardown(t) + t.Skip() + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + stateMacAddress := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Ethernet().MacAddress().State()) + if _, err := net.ParseMAC(stateMacAddress); err != nil { + t.Errorf("Invalid MAC address format received for interface %v! got:%v", inbandSwIntfName, stateMacAddress) + } +} + +// TestGNMIInbandSwIntfOperStatus - Check inband sw interface Oper-Status is expected value. +// TODO: remove this comment: Currently this test fails due to b/194325182 +func TestGNMIInbandSwIntfOperStatus(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("9086dbae-2636-400b-8381-f6ff7e5b0772").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + if operStatus := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + t.Errorf("%v OperStatus is %v, wanted UP", inbandSwIntfName, operStatus) + } +} + +// TestGNMIInbandSwIntfSetIPv4Addr -- Set and check IPv4 address on the inband sw interface +func TestGNMIInbandSwIntfSetIPv4Addr(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("e99b7744-c11d-458a-84dd-5da351792d04").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + d := &oc.Root{} + iface := d.GetOrCreateInterface(inbandSwIntfName).GetOrCreateSubinterface(interfaceIndex) + + // Set IPv4 address for the loopback0 interface. + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(newConfiguredIPv4Path) + newV4.Ip = &newConfiguredIPv4Path + newV4.PrefixLength = &newConfiguredIPv4PrefixLength + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(newConfiguredIPv4Path).Config(), newV4) + + gnmi.Await(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(newConfiguredIPv4Path).Ip().State(), 5*time.Second, newConfiguredIPv4Path) + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(newConfiguredIPv4Path).PrefixLength().State()); got != newConfiguredIPv4PrefixLength { + t.Errorf("IP prefix length configure failed! got:%v, want:%v", got, newConfiguredIPv4PrefixLength) + } +} + +// TestGNMIInbandSwIntfSetIPv6Addr -- Set and check IPv6 address on the inband sw interface +func TestGNMIInbandSwIntfSetIPv6Addr(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("8bb6c702-905d-4d08-b40b-ca0917ed4511").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + d := &oc.Root{} + iface := d.GetOrCreateInterface(inbandSwIntfName).GetOrCreateSubinterface(interfaceIndex) + + // Set IPv6 address for the loopback0 interface. + newV6 := iface.GetOrCreateIpv6().GetOrCreateAddress(newConfiguredIPv6Path) + newV6.Ip = &newConfiguredIPv6Path + newV6.PrefixLength = &newConfiguredIPv6PrefixLength + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(newConfiguredIPv6Path).Config(), newV6) + + gnmi.Await(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(newConfiguredIPv6Path).Ip().State(), 5*time.Second, newConfiguredIPv6Path) + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(newConfiguredIPv6Path).PrefixLength().State()); got != newConfiguredIPv6PrefixLength { + t.Errorf("IPv6 prefix length configure failed! got:%v, want:%v", got, newConfiguredIPv6PrefixLength) + } +} + +// TestGNMIInbandSwIntfSetInvalidIPv4AddrAndPrefixLength -- Set and check IPv4 address with invalid address and prefix length on the inband sw interface +func TestGNMIInbandSwIntfSetInvalidIPv4AddrOrPrefixLength(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("72e632a0-b7fc-47a6-8fb4-906363e995cb").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + d := &oc.Root{} + iface := d.GetOrCreateInterface(inbandSwIntfName).GetOrCreateSubinterface(interfaceIndex) + + // Set invalid IPv4 address on the loopback0 interface. + var invalidIPPaths = []string{"255.123.231.69", "0.125.120.136"} + for _, invalidIPPath := range invalidIPPaths { + + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(invalidIPPath) + newV4.Ip = &invalidIPPath + newV4.PrefixLength = &newConfiguredIPv4PrefixLength + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(invalidIPPath).Config(), newV4) + }) + } + + // Verify the IP address not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(newConfiguredIPv4Path).Ip().State()); got != newConfiguredIPv4Path { + t.Errorf("Negative testing for IP address configure failed! got:%v, want:%v", got, newConfiguredIPv4Path) + } + + // Set IPv4 address with invalid prefix length for the loopback0 interface. + var tryConfiguredIPv4Path = "10.10.60.30" + var badConfiguredIPv4PrefixLength = uint8(24) + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(tryConfiguredIPv4Path) + newV4.Ip = &tryConfiguredIPv4Path + newV4.PrefixLength = &badConfiguredIPv4PrefixLength + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(tryConfiguredIPv4Path).Config(), newV4) + }) + + // Verify the IP address and prefix length are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(newConfiguredIPv4Path).Ip().State()); got != newConfiguredIPv4Path { + t.Errorf("Negative testing for IP address configure failed! got:%v, want:%v", got, newConfiguredIPv4Path) + } + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv4().Address(newConfiguredIPv4Path).PrefixLength().State()); got != newConfiguredIPv4PrefixLength { + t.Errorf("Negative IP prefix length configure failed! got:%v, want:%v", got, newConfiguredIPv4PrefixLength) + } +} + +// TestGNMIInbandSwIntfSetInvalidIPv6AddrAndPrefixLength -- Set and check IPv6 invalid address and invalid prefix length on the inband sw interface +func TestGNMIInbandSwIntfSetInvalidIPv6AddrOrPrefixLength(t *testing.T) { + // Report results in TestTracker at the end + defer testhelper.NewTearDownOptions(t).WithID("1e175d69-8968-4c0c-a34b-33d35969c9e0").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + d := &oc.Root{} + iface := d.GetOrCreateInterface(inbandSwIntfName).GetOrCreateSubinterface(interfaceIndex) + + // Set invalid IPv6 address + var invalidIPPath = "ffff:ffff:ffff:ffff:ffff:f567:67ff:befa:458f" + newV6 := iface.GetOrCreateIpv6().GetOrCreateAddress(invalidIPPath) + newV6.Ip = &invalidIPPath + newV6.PrefixLength = &newConfiguredIPv6PrefixLength + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(invalidIPPath).Config(), newV6) + }) + + // Verify the IPv6 address not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(newConfiguredIPv6Path).Ip().State()); got != newConfiguredIPv6Path { + t.Errorf("Negative testing for IPv6 address configure failed! got:%v, want:%v", got, newConfiguredIPv6Path) + } + + // Set IPv6 address with invalid prefix length for the loopback0 interface. + var tryConfiguredIPv6Path = "3123::4567" + var badConfiguredIPv6PrefixLength = uint8(80) + newV6 = iface.GetOrCreateIpv6().GetOrCreateAddress(tryConfiguredIPv6Path) + newV6.Ip = &tryConfiguredIPv6Path + newV6.PrefixLength = &badConfiguredIPv6PrefixLength + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(tryConfiguredIPv6Path).Config(), newV6) + }) + + // Verify the IPv6 address and prefix length are not changed. + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(newConfiguredIPv6Path).Ip().State()); got != newConfiguredIPv6Path { + t.Errorf("Negative testing for IPv6 address configure failed! got:%v, want:%v", got, newConfiguredIPv6Path) + } + if got := gnmi.Get(t, dut, gnmi.OC().Interface(inbandSwIntfName).Subinterface(interfaceIndex).Ipv6().Address(newConfiguredIPv6Path).PrefixLength().State()); got != newConfiguredIPv6PrefixLength { + t.Errorf("Negative IPv6 prefix length configure failed! got:%v, want:%v", got, newConfiguredIPv6PrefixLength) + } +} diff --git a/tests/installation_test.go b/tests/installation_test.go new file mode 100644 index 0000000..f934964 --- /dev/null +++ b/tests/installation_test.go @@ -0,0 +1,33 @@ +package installation_test + +import ( + "testing" + "time" + + syspb "github.com/openconfig/gnoi/system" + "github.com/openconfig/ondatra" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func TestConfigInstallationSuccess(t *testing.T) { + ttID := "0dedda87-1b76-40a2-8712-24c1572587ee" + defer testhelper.NewTearDownOptions(t).WithID(ttID).Teardown(t) + dut := ondatra.DUT(t, "DUT") + err :=testhelper.ConfigPush(t, dut, nil) + if err != nil { + t.Fatalf("switch config push failed due to err : %v", err) + } + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30*time.Second).WithRequest(syspb.RebootMethod_COLD).WithLatencyMeasurement(ttID, "gNOI Reboot With Type: "+syspb.RebootMethod_COLD.String()) + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Failed to reboot DUT: %v", err) + } +} diff --git a/tests/lacp_test.go b/tests/lacp_test.go new file mode 100644 index 0000000..82e96af --- /dev/null +++ b/tests/lacp_test.go @@ -0,0 +1,683 @@ +package lacp_test + +import ( + "fmt" + "sort" + "strings" + "testing" + "time" + + log "github.com/golang/glog" + "github.com/google/go-cmp/cmp" + "github.com/openconfig/ondatra" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" +) + +// gNMI can cache local state for up to 10 seconds. We therefore set our timeout to a little longer +// to handle any edge cases when verifying state. +const defaultGNMIWait = 15 * time.Second + +// IEEE 802.3ad defines the Link Aggregation standard used by LACP where connected ports can +// experimental control packets between each other. Based on these packets the switch can group matching +// ports into a LAG/Trunk/PortChannel. +// +// Local state is maintained for each member of a LAG to monitor the health of that given member. +type lacpMemberState struct { + activity oc.E_Lacp_LacpActivityType + timeout oc.E_Lacp_LacpTimeoutType + aggregatable bool + synchronization oc.E_Lacp_LacpSynchronizationType + collecting bool + distributing bool +} + +// Wait for the switch state of a specific PortChannel member to converge to a users expectations. +// If the switch state does not converge return a string detailing the difference between the final +// state received, and what the user wanted. +func compareLacpMemberState(t *testing.T, dut *ondatra.DUTDevice, pcName string, memberName string, want lacpMemberState) string { + t.Helper() + + // Wait for the switch value to match our expectation. + predicate := func(val *ygnmi.Value[*oc.Lacp_Interface_Member]) bool { + currentVal, present := val.Val() + if !present { + return false + } + return currentVal.GetActivity() == want.activity && + currentVal.GetTimeout() == want.timeout && + currentVal.GetAggregatable() == want.aggregatable && + currentVal.GetSynchronization() == want.synchronization && + currentVal.GetCollecting() == want.collecting && + currentVal.GetDistributing() == want.distributing + } + lastVal, match := gnmi.Watch(t, dut, gnmi.OC().Lacp().Interface(pcName).Member(memberName).State(), defaultGNMIWait, predicate).Await(t) + + state, ok := lastVal.Val() + if !ok { + return "no value for lacp interface member" + } + var diff strings.Builder + if !match { + fmt.Fprintf(&diff, "%v:%v:%v (-want, +got)", dut.Name(), pcName, memberName) + + if state.Activity == want.activity { + fmt.Fprintf(&diff, "\nactivity: %v", state.Activity) + } else if state.Activity == oc.Lacp_LacpActivityType_UNSET { + fmt.Fprintf(&diff, "\nactivity: -%v, +(unset)", want.activity) + } else { + fmt.Fprintf(&diff, "\nactivity: -%v, +%v", want.activity, state.Activity) + } + + if state.Timeout == want.timeout { + fmt.Fprintf(&diff, "\ntimeout: %v", state.Timeout) + } else if state.Timeout == oc.Lacp_LacpTimeoutType_UNSET { + fmt.Fprintf(&diff, "\ntimeout: -%v, +(unset)", want.timeout) + } else { + fmt.Fprintf(&diff, "\ntimeout: -%v, +%v", want.timeout, state.Timeout) + } + + if state.Aggregatable != nil && *state.Aggregatable == want.aggregatable { + fmt.Fprintf(&diff, "\naggregatable: %v", *state.Aggregatable) + } else if state.Aggregatable == nil { + fmt.Fprintf(&diff, "\naggregatable: -%v, +(unset)", want.aggregatable) + } else { + fmt.Fprintf(&diff, "\naggregatable: -%v, +%v", want.aggregatable, *state.Aggregatable) + } + + if state.Synchronization == want.synchronization { + fmt.Fprintf(&diff, "\nsynchronization: %v", state.Synchronization) + } else if state.Synchronization == oc.Lacp_LacpSynchronizationType_UNSET { + fmt.Fprintf(&diff, "\nsynchronization: -%v, +(unset)", want.synchronization) + } else { + fmt.Fprintf(&diff, "\nsynchronization: -%v, +%v", want.synchronization, state.Synchronization) + } + + if state.Collecting != nil && *state.Collecting == want.collecting { + fmt.Fprintf(&diff, "\ncollecting: %v", state.Collecting) + } else if state.Collecting == nil { + fmt.Fprintf(&diff, "\ncollecting: -%v, +(unset)", want.collecting) + } else { + fmt.Fprintf(&diff, "\ncollecting: -%v, -%v", want.collecting, *state.Collecting) + } + + if state.Distributing != nil && *state.Distributing == want.distributing { + fmt.Fprintf(&diff, "\ndistributing: %v", *state.Distributing) + } else if state.Distributing == nil { + fmt.Fprintf(&diff, "\ndistributing: -%v, +(unset)", want.distributing) + } else { + fmt.Fprintf(&diff, "\ndistributing: -%v, +%v", want.distributing, *state.Distributing) + } + } + return diff.String() +} + +// Blocking state happens when one side of the LACP connection is up, but the other is not. For +// example, a port is down, or not yet configured. When this happens we expect the member to still +// be active and aggregatable (i.e. waiting for the other end to come up), but not yet in-sync or +// collecting/distributing traffic. +func verifyBlockingState(t *testing.T, dut *ondatra.DUTDevice, pcName string, memberName string) error { + t.Helper() + + want := lacpMemberState{ + activity: oc.Lacp_LacpActivityType_ACTIVE, + timeout: oc.Lacp_LacpTimeoutType_LONG, + aggregatable: true, + synchronization: oc.Lacp_LacpSynchronizationType_OUT_SYNC, + collecting: false, + distributing: false, + } + + // gNMI does not support ON_CHANGE events for LACP paths, and SAMPLING can fatally fail if an + // entry doesn't yet exist (i.e. we call this too quickly after sending a config). So to prevent + // flakes we run this check twice in a row. + if firstDiff := compareLacpMemberState(t, dut, pcName, memberName, want); firstDiff != "" { + log.Warningf("Failed first blocking check: %s", firstDiff) + if secondDiff := compareLacpMemberState(t, dut, pcName, memberName, want); secondDiff != "" { + return errors.New(secondDiff) + } + } + return nil +} + +// In-Sync state happens when both side of the LACP connection are up and healthy. When in this +// state we expect the member to be active, aggregatable, in-sync, collecting, and distributing +// traffic. +func verifyInSyncState(t *testing.T, dut *ondatra.DUTDevice, pcName string, memberName string) error { + t.Helper() + + want := lacpMemberState{ + activity: oc.Lacp_LacpActivityType_ACTIVE, + timeout: oc.Lacp_LacpTimeoutType_LONG, + aggregatable: true, + synchronization: oc.Lacp_LacpSynchronizationType_IN_SYNC, + collecting: true, + distributing: true, + } + + // gNMI does not support ON_CHANGE events for LACP paths, and SAMPLING can fatally fail if an + // entry doesn't yet exist (i.e. we call this too quickly after sending a config). So to prevent + // flakes we run this check twice in a row. + if firstDiff := compareLacpMemberState(t, dut, pcName, memberName, want); firstDiff != "" { + log.Warningf("Failed first in-sync check: %s", firstDiff) + if secondDiff := compareLacpMemberState(t, dut, pcName, memberName, want); secondDiff != "" { + return errors.New(secondDiff) + } + } + return nil +} + +// gNMI does not specify an ordering for the member list of a PortChannel. To make tests +// reproducible we need to sort the member lists before comparing. +func comparePortChannelMemberList(t *testing.T, timeout time.Duration, dut *ondatra.DUTDevice, pcName string, members []string) error { + t.Helper() + + // Users do not have to pre-sort their list. + sort.Strings(members) + + predicate := func(val *ygnmi.Value[[]string]) bool { + got, present := val.Val() + // If the value isn't present then simply return false. + if !present { + return false + } + + // Otherwise, sort the values from the switch, and compare them to the expectations. + sort.Strings(got) + return cmp.Equal(members, got) + } + + // gNMI does not support ON_CHANGE events for LACP paths, and SAMPLING can fatally fail if an + // entry doesn't yet exist (i.e. we call this too quickly after sending a config). So to prevent + // flakes we run this check twice in a row. + if lastVal, matched := gnmi.Watch(t, dut, gnmi.OC().Interface(pcName).Aggregation().Member().State(), timeout, predicate).Await(t); !matched { + log.Warningf("Failed first membership check: %v", lastVal) + if lastVal, again := gnmi.Watch(t, dut, gnmi.OC().Interface(pcName).Aggregation().Member().State(), timeout, predicate).Await(t); !again { + return errors.Errorf("member state does not match %v:%v", members, lastVal) + } + } + return nil +} + +// Translates an openconfig ETHERNET_SPEED into Mbps which can be used to verify a LAG's speed. +func ethernetPortSpeedToMbps(speed oc.E_IfEthernet_ETHERNET_SPEED) (uint32, error) { + // Returns bits/sec. + bps, err := testhelper.EthernetSpeedToUint64(speed) + if err != nil { + return 0, err + } + return uint32(bps / 1_000_000), nil +} + +// Fetches the configured PortSpeed for a list of ports, and aggregates the values together. Can be +// be used to verify a PortChannel speed matches the total of all its member ports. +func aggregatedPortSpeed(t *testing.T, dut *ondatra.DUTDevice, ports []string) (uint32, error) { + lagSpeed := uint32(0) + + for _, port := range ports { + portSpeed, err := ethernetPortSpeedToMbps(gnmi.Get(t, dut, gnmi.OC().Interface(port).Ethernet().PortSpeed().Config())) + if err != nil { + return 0, errors.Wrapf(err, "could not get port speed for %s", port) + } + lagSpeed += portSpeed + } + + return lagSpeed, nil +} + +// Used by go/ondatra to automatically reserve an available testbed. +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func TestCreatingPortChannel(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b280a73c-c8e9-411b-b5ef-a22240463377").Teardown(t) + + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 2) + if err != nil { + t.Fatalf("Failed to get enough peer ports: %v", err) + } + t.Logf("Using peer ports: %v", peerPorts) + + // The PortChannel configs will be the same on both the host and peer devices so we can reuse + // them. Since this is a sanity test to verify PortChannels can be created we manually set most of + // the configuration variables. + portChannel := "PortChannel200" + portChannelID := uint32(2001) + portChannelDescription := "PortChanne200 used for sanity testing." + portChannelMinLinks := uint16(2) + portChannelMtu := uint16(1514) + lacpInterval := oc.Lacp_LacpPeriodType_FAST + lacpMode := oc.Lacp_LacpActivityType_ACTIVE + lacpKey := uint16(85) + + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel) + portChannelConfig.Id = &portChannelID + portChannelConfig.Mtu = &portChannelMtu + portChannelConfig.Description = &portChannelDescription + portChannelConfig.Aggregation.MinLinks = &portChannelMinLinks + portChannelConfigs := map[string]*oc.Interface{portChannel: &portChannelConfig} + + lacpConfig := testhelper.GenerateLACPInterface(portChannel) + lacpConfig.Interval = lacpInterval + + lacpConfig.LacpMode = lacpMode + var lacpConfigs oc.Lacp + lacpConfigs.AppendInterface(&lacpConfig) + + deviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &lacpConfigs, + } + + // Push the same device config to both switches under test. + gnmi.Replace(t, host, gnmi.OC().Config(), deviceConfig) + testhelper.UpdateLacpKey(t, host, portChannel, lacpKey) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel, err) + } + }() + gnmi.Replace(t, peer, gnmi.OC().Config(), deviceConfig) + testhelper.UpdateLacpKey(t, peer, portChannel, lacpKey) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, peer, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", peer.Name(), portChannel, err) + } + }() + + // Ethernet ports are added to the PortChannel with its ID. Once this is done we expect the + // PortChannel to be active. Notice that the LAG member's MTU must match the PortChannel's. + // Otherwise, the FE will reject the request. + origMtuHostPort0 := gnmi.Get(t, host, gnmi.OC().Interface(peerPorts[0].Host).Mtu().Config()) + origMtuHostPort1 := gnmi.Get(t, host, gnmi.OC().Interface(peerPorts[1].Host).Mtu().Config()) + defer func() { + gnmi.Replace(t, host, gnmi.OC().Interface(peerPorts[0].Host).Mtu().Config(), origMtuHostPort0) + gnmi.Replace(t, host, gnmi.OC().Interface(peerPorts[1].Host).Mtu().Config(), origMtuHostPort1) + }() + gnmi.Replace(t, host, gnmi.OC().Interface(peerPorts[0].Host).Mtu().Config(), portChannelMtu) + gnmi.Replace(t, host, gnmi.OC().Interface(peerPorts[1].Host).Mtu().Config(), portChannelMtu) + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[0].Host, peerPorts[1].Host) + + origMtuPeerPort0 := gnmi.Get(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Mtu().Config()) + origMtuPeerPort1 := gnmi.Get(t, peer, gnmi.OC().Interface(peerPorts[1].Peer).Mtu().Config()) + defer func() { + gnmi.Replace(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Mtu().Config(), origMtuPeerPort0) + gnmi.Replace(t, peer, gnmi.OC().Interface(peerPorts[1].Peer).Mtu().Config(), origMtuPeerPort1) + }() + gnmi.Replace(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Mtu().Config(), portChannelMtu) + gnmi.Replace(t, peer, gnmi.OC().Interface(peerPorts[1].Peer).Mtu().Config(), portChannelMtu) + testhelper.AssignPortsToAggregateID(t, peer, portChannel, peerPorts[0].Peer, peerPorts[1].Peer) + + // Verify that the Ethernet interfaces are enabled, and assigned to the correct PortChannel. + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Ethernet().AggregateId().State(), defaultGNMIWait, portChannel) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[1].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[1].Host).Ethernet().AggregateId().State(), defaultGNMIWait, portChannel) + + // Verify the PortChannel interface state. + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).AdminStatus().State(), defaultGNMIWait, oc.Interface_AdminStatus_UP) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).OperStatus().State(), defaultGNMIWait, oc.Interface_OperStatus_UP) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Type().State(), defaultGNMIWait, oc.IETFInterfaces_InterfaceType_ieee8023adLag) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Id().State(), defaultGNMIWait, portChannelID) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Description().State(), defaultGNMIWait, portChannelDescription) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Mtu().State(), defaultGNMIWait, portChannelMtu) + expectedHostPorts := []string{peerPorts[0].Host, peerPorts[1].Host} + if err := comparePortChannelMemberList(t, defaultGNMIWait, host, portChannel, expectedHostPorts); err != nil { + t.Errorf("PortChannel member list is invalid: %v", err) + } + // expectedLagSpeed, err := aggregatedPortSpeed(t, host, expectedHostPorts) + // if err != nil { + // t.Fatalf("Could not get expected LAG speed: %v", err) + // } + + // TODO: enable after the bug is fixed. + // Monitoring tools will SAMPLE data from /interfaces/interface[name=]/aggregation/state/, + // and the gNMI FE does not support ON_CHANGE in this case. So we update the subscription mode. + //gnmi.Await(t, host.GNMIOpts().WithYGNMIOpts(ygnmi.WithSubscriptionMode(gpb.SubscriptionMode_SAMPLE)), gnmi.OC().Interface(portChannel).Aggregation().LagSpeed().State(), defaultGNMIWait, expectedLagSpeed) + gnmi.Await(t, host.GNMIOpts().WithYGNMIOpts(ygnmi.WithSubscriptionMode(gpb.SubscriptionMode_SAMPLE)), gnmi.OC().Interface(portChannel).Aggregation().MinLinks().State(), defaultGNMIWait, portChannelMinLinks) + gnmi.Await(t, host.GNMIOpts().WithYGNMIOpts(ygnmi.WithSubscriptionMode(gpb.SubscriptionMode_SAMPLE)), gnmi.OC().Interface(portChannel).Aggregation().LagType().State(), defaultGNMIWait, oc.IfAggregate_AggregationType_LACP) + + // Verify the LACP settings for the PortChannel. + gnmi.Await(t, host, gnmi.OC().Lacp().Interface(portChannel).Interval().State(), defaultGNMIWait, lacpInterval) + gnmi.Await(t, host, gnmi.OC().Lacp().Interface(portChannel).LacpMode().State(), defaultGNMIWait, lacpMode) + testhelper.AwaitLacpKey(t, host, portChannel, defaultGNMIWait, lacpKey) + + // We don't explicitly configure the LACP system MAC or priority. Therefore, the MAC should match + // whatever the ethernet ports were configured to, and the priority will default to 0xFFFF. + expectedSystemMac := gnmi.Get(t, host, gnmi.OC().Interface(peerPorts[0].Host).Ethernet().MacAddress().State()) + gnmi.Await(t, host, gnmi.OC().Lacp().Interface(portChannel).SystemIdMac().State(), defaultGNMIWait, expectedSystemMac) + gnmi.Await(t, host, gnmi.OC().Lacp().Interface(portChannel).SystemPriority().State(), defaultGNMIWait, 0xFFFF) + + // Verify the LACP settings for each member of the PortChannel. + if err := verifyInSyncState(t, host, portChannel, peerPorts[0].Host); err != nil { + t.Errorf("LACP state is not in-sync: %v", err) + } + gnmi.Await(t, host, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[0].Host).SystemId().State(), defaultGNMIWait, expectedSystemMac) + gnmi.Await(t, host, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[0].Host).OperKey().State(), defaultGNMIWait, lacpKey) + gnmi.Await(t, peer, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[0].Peer).PartnerId().State(), defaultGNMIWait, expectedSystemMac) + gnmi.Await(t, peer, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[0].Peer).PartnerKey().State(), defaultGNMIWait, lacpKey) +} + +func TestAddingInterfaceToAnExistingPortChannel(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("cda6bc3d-9bfa-44f2-8851-53d42ab2c5bb").Teardown(t) + + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 2) + if err != nil { + t.Fatalf("Failed to get enough peer ports: %v", err) + } + t.Logf("Using peer ports: %v", peerPorts) + + // We bring up one PortChannel on both the host and peer device so we can reuse the same device + // configuration on both without issue. + portChannel := "PortChannel200" + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel) + portChannelConfigs := map[string]*oc.Interface{portChannel: &portChannelConfig} + + lacpConfig := testhelper.GenerateLACPInterface(portChannel) + var lacpConfigs oc.Lacp + lacpConfigs.AppendInterface(&lacpConfig) + + deviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &lacpConfigs, + } + + // Push the PortChannel configs and clean them up after the test finishes so they won't affect + // future tests + gnmi.Replace(t, host, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel, err) + } + }() + gnmi.Replace(t, peer, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, peer, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", peer.Name(), portChannel, err) + } + }() + + // Assign 1 port to each PortChannel so the interfaces become active. + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[0].Host) + testhelper.AssignPortsToAggregateID(t, peer, portChannel, peerPorts[0].Peer) + + // Verify that the Ethernet and PortChannel interfaces are enabled, and that the correct member + // port is assigned. + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Ethernet().AggregateId().State(), defaultGNMIWait, portChannel) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Enabled().State(), defaultGNMIWait, true) + if err := comparePortChannelMemberList(t, defaultGNMIWait, host, portChannel, []string{peerPorts[0].Host}); err != nil { + t.Errorf("PortChannel member list is invalid: %v", err) + } + + // Assign additional ports to each PortChannel. + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[1].Host) + testhelper.AssignPortsToAggregateID(t, peer, portChannel, peerPorts[1].Peer) + + // Verify that the new Ethernet interface is enabled, and the PortChannel has the correct member + // ports assigned. + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Ethernet().AggregateId().State(), defaultGNMIWait, portChannel) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[1].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[1].Host).Ethernet().AggregateId().State(), defaultGNMIWait, portChannel) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Enabled().State(), defaultGNMIWait, true) + if err := comparePortChannelMemberList(t, defaultGNMIWait, host, portChannel, []string{peerPorts[0].Host, peerPorts[1].Host}); err != nil { + t.Errorf("%s member list is invalid: %v", portChannel, err) + } +} + +func TestRemoveInterfaceFromAnExistingPortChannel(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("7e4936d3-2615-40b4-9cc0-e648c301f5df").Teardown(t) + + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 2) + if err != nil { + t.Fatalf("Failed to get enough peer ports: %v", err) + } + t.Logf("Using peer ports: %v", peerPorts) + + // We bring up one PortChannel on both the host and peer device so we can reuse the same device + // configuration on both without issue. + portChannel := "PortChannel200" + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel) + portChannelConfigs := map[string]*oc.Interface{portChannel: &portChannelConfig} + + var lacpConfigs oc.Lacp + portChannelLACPConfig := testhelper.GenerateLACPInterface(portChannel) + lacpConfigs.AppendInterface(&portChannelLACPConfig) + + deviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &lacpConfigs, + } + + // Push the PortChannel configs and clean them up after the test finishes so they won't affect + // future tests. + gnmi.Replace(t, host, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel, err) + } + }() + gnmi.Replace(t, peer, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, peer, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", peer.Name(), portChannel, err) + } + }() + + // Assign both member ports to each PortChannel, and verify their membership before trying to + // remove one. + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[0].Host, peerPorts[1].Host) + testhelper.AssignPortsToAggregateID(t, peer, portChannel, peerPorts[0].Peer, peerPorts[1].Peer) + if err := comparePortChannelMemberList(t, defaultGNMIWait, host, portChannel, []string{peerPorts[0].Host, peerPorts[1].Host}); err != nil { + t.Errorf("PortChannel member list is invalid: %v", err) + } + + // Remove a port from the PortChannel and verify it was removed from the member list. + gnmi.Delete(t, host, gnmi.OC().Interface(peerPorts[1].Host).Ethernet().AggregateId().Config()) + gnmi.Delete(t, peer, gnmi.OC().Interface(peerPorts[1].Peer).Ethernet().AggregateId().Config()) + if err := comparePortChannelMemberList(t, defaultGNMIWait, host, portChannel, []string{peerPorts[0].Host}); err != nil { + t.Errorf("PortChannel member list is invalid: %v", err) + } +} + +func TestLacpConfiguredOnOnlyOneSwitch(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("71fb9896-993a-4a17-ab47-07b52cc184ee").Teardown(t) + + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 2) + if err != nil { + t.Fatalf("Failed to get enough peer ports: %v", err) + } + t.Logf("Using peer ports: %v", peerPorts) + + // We bring up the PortChannel on just the host. + portChannel := "PortChannel200" + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel) + portChannelConfigs := map[string]*oc.Interface{portChannel: &portChannelConfig} + + lacpConfig := testhelper.GenerateLACPInterface(portChannel) + var lacpConfigs oc.Lacp + lacpConfigs.AppendInterface(&lacpConfig) + + deviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &lacpConfigs, + } + gnmi.Replace(t, host, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel, err) + } + }() + + // Only assign the host port to a PortChannel. + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[0].Host, peerPorts[1].Host) + + // Ensure ports are enabled before trying to verify state. + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[1].Host).Enabled().State(), defaultGNMIWait, true) + + if err := verifyBlockingState(t, host, portChannel, peerPorts[0].Host); err != nil { + t.Errorf("LACP state is not blocking: %v", err) + } + if err := verifyBlockingState(t, host, portChannel, peerPorts[1].Host); err != nil { + t.Errorf("LACP state is not blocking: %v", err) + } +} + +func TestMembersArePartiallyConfigured(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("42eea9f9-043c-45c4-95fd-5c1e00fef959").Teardown(t) + + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 2) + if err != nil { + t.Fatalf("Failed to get enough peer ports: %v", err) + } + t.Logf("Using peer ports: %v", peerPorts) + + // We bring up one PortChannel on both the host and peer device so we can reuse the same device + // configuration on both without issue. + portChannel := "PortChannel200" + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel) + portChannelConfigs := map[string]*oc.Interface{portChannel: &portChannelConfig} + + lacpConfig := testhelper.GenerateLACPInterface(portChannel) + var lacpConfigs oc.Lacp + lacpConfigs.AppendInterface(&lacpConfig) + + deviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &lacpConfigs, + } + + // Push the PortChannel configs and clean them up after the test finishes so they won't affect + // future tests + gnmi.Replace(t, host, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel, err) + } + }() + gnmi.Replace(t, peer, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, peer, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", peer.Name(), portChannel, err) + } + }() + + // On the host assign both ports to the PortChannel, but on the peer only assign 1. + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[0].Host, peerPorts[1].Host) + testhelper.AssignPortsToAggregateID(t, peer, portChannel, peerPorts[0].Peer) + + // Ensure ports are enabled before trying to verify state. + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[0].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, host, gnmi.OC().Interface(peerPorts[1].Host).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Enabled().State(), defaultGNMIWait, true) + + if err := verifyInSyncState(t, host, portChannel, peerPorts[0].Host); err != nil { + t.Errorf("LACP state is not in-sync: %v", err) + } + if err := verifyInSyncState(t, peer, portChannel, peerPorts[0].Peer); err != nil { + t.Errorf("LACP state is not in-sync: %v", err) + } + if err := verifyBlockingState(t, host, portChannel, peerPorts[1].Host); err != nil { + t.Errorf("LACP state is not blocking: %v", err) + } +} + +func TestPortDownEvent(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("8b585121-80c1-4ca1-9847-5433ded2ebe6").Teardown(t) + + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 2) + if err != nil { + t.Fatalf("Failed to get enough peer ports: %v", err) + } + t.Logf("Using peer ports: %v", peerPorts) + + // The same PortChannel settings will be used on the host and peer devices. + portChannel1 := "PortChannel200" + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel1) + portChannelConfigs := map[string]*oc.Interface{portChannel1: &portChannelConfig} + + var lacpConfigs oc.Lacp + lacpConfig := testhelper.GenerateLACPInterface(portChannel1) + lacpConfigs.AppendInterface(&lacpConfig) + + deviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &lacpConfigs, + } + gnmi.Replace(t, host, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel1); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel1, err) + } + }() + gnmi.Replace(t, peer, gnmi.OC().Config(), deviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, peer, portChannel1); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", peer.Name(), portChannel1, err) + } + }() + + // Assign the port to each PortChannel and wait for the links to become active. + testhelper.AssignPortsToAggregateID(t, host, portChannel1, peerPorts[0].Host, peerPorts[1].Host) + testhelper.AssignPortsToAggregateID(t, peer, portChannel1, peerPorts[0].Peer, peerPorts[1].Peer) + gnmi.Await(t, host, gnmi.OC().Interface(portChannel1).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, peer, gnmi.OC().Interface(portChannel1).Enabled().State(), defaultGNMIWait, true) + + // Bring the port down on the peer side. + gnmi.Replace(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Enabled().Config(), false) + defer func() { + gnmi.Replace(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Enabled().Config(), true) + }() + + // Wait for the port to go down on the peer then verify the host side is in a blocking state. + gnmi.Await(t, peer, gnmi.OC().Interface(peerPorts[0].Peer).Enabled().State(), defaultGNMIWait, false) + if err := verifyBlockingState(t, host, portChannel1, peerPorts[0].Host); err != nil { + t.Errorf("LACP state is not blocking: %v", err) + } +} diff --git a/tests/lacp_timeout_test.go b/tests/lacp_timeout_test.go new file mode 100644 index 0000000..4c0cdcc --- /dev/null +++ b/tests/lacp_timeout_test.go @@ -0,0 +1,203 @@ +package lacp_timeout_test + +import ( + "crypto/rand" + "math/big" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" +) + +// gNMI can cache local state for up to 10 seconds. We therefore set our timeout to a little longer +// to handle any edge cases when verifying state. +const defaultGNMIWait = 15 * time.Second + +// Convert LACP period types to strings for use in parameterized test names. The output should +// follow CamelCase styles. +func lacpPeriodTypeToString(period oc.E_Lacp_LacpPeriodType) string { + if period == oc.Lacp_LacpPeriodType_FAST { + return "Fast" + } else if period == oc.Lacp_LacpPeriodType_SLOW { + return "Slow" + } + return "Unknown" +} + +// Convert LACP activity types to strings for use in parameterized test names. The output should +// follow CamelCase styles. +func lacpActivityTypeToString(activity oc.E_Lacp_LacpActivityType) string { + if activity == oc.Lacp_LacpActivityType_ACTIVE { + return "Active" + } else if activity == oc.Lacp_LacpActivityType_PASSIVE { + return "Passive" + } + return "Unknown" +} + +// Check if the number of packets is within an acceptable range for 1 minute given the LACP Interval. +func acceptableLACPDUPacketCountForOneMinute(period oc.E_Lacp_LacpPeriodType, count uint64) error { + switch period { + case oc.Lacp_LacpPeriodType_FAST: + // When the period is FAST we expect around 1 packet per-second. So ~60 packets. + if count < 55 || count > 65 { + return errors.Errorf("outside range [55, 65]: %v.", count) + } + case oc.Lacp_LacpPeriodType_SLOW: + // When the period is SLOW we expect around 1 packet every 30 seconds. So ~2 packets. + if count < 1 || count > 5 { + return errors.Errorf("outside range [1, 5]: %v.", count) + } + default: + return errors.Errorf("unhandled period type: %v", period) + } + + return nil +} + +// Verifies the LACP timeout pings are working as expected. Pings can be sent either once every +// second (i.e. FAST), or once every 30 seconds (i.e. SLOW). This test allows for some variability +// in the exact number of pings sent and received, but will fail if the number isn't roughly what we +// expect based on the period type. +func verifyLACPTimeout(t *testing.T, hostActivity oc.E_Lacp_LacpActivityType, hostPeriod oc.E_Lacp_LacpPeriodType, peerActivity oc.E_Lacp_LacpActivityType, peerPeriod oc.E_Lacp_LacpPeriodType) error { + host := ondatra.DUT(t, "DUT") + peer := ondatra.DUT(t, "CONTROL") + t.Logf("Host Device: %v", host.Name()) + t.Logf("Peer Device: %v", peer.Name()) + + // Find a set of peer ports between the 2 switches. Notice this test uses multiple port to ensure + // the correct number of LACPDU packets are being sent per member. + peerPorts, err := testhelper.PeerPortGroupWithNumMembers(t, host, peer, 4) + if err != nil { + return err + } + t.Logf("Using peer ports: %v", peerPorts) + + // The interface config for PortChannels will be the same on both switches. + portChannel := "PortChannel200" + portChannelConfig := testhelper.GeneratePortChannelInterface(portChannel) + portChannelConfigs := map[string]*oc.Interface{portChannel: &portChannelConfig} + + // Configure the host side LACP settings. + hostLACPConfig := testhelper.GenerateLACPInterface(portChannel) + hostLACPConfig.LacpMode = hostActivity + hostLACPConfig.Interval = hostPeriod + var hostLACPConfigs oc.Lacp + hostLACPConfigs.AppendInterface(&hostLACPConfig) + hostDeviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &hostLACPConfigs, + } + gnmi.Replace(t, host, gnmi.OC().Config(), hostDeviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, host, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", host.Name(), portChannel, err) + } + }() + + // Configure the peer side LACP settings. + peerLACPConfig := testhelper.GenerateLACPInterface(portChannel) + peerLACPConfig.LacpMode = peerActivity + peerLACPConfig.Interval = peerPeriod + var peerLACPConfigs oc.Lacp + peerLACPConfigs.AppendInterface(&peerLACPConfig) + peerDeviceConfig := &oc.Root{ + Interface: portChannelConfigs, + Lacp: &peerLACPConfigs, + } + gnmi.Replace(t, peer, gnmi.OC().Config(), peerDeviceConfig) + defer func() { + if err := testhelper.RemovePortChannelFromDevice(t, defaultGNMIWait, peer, portChannel); err != nil { + t.Fatalf("Failed to remove %v:%v: %v", peer.Name(), portChannel, err) + } + }() + + // Assign all ethernet ports to the port channels on each switch so we will be getting multiple + // LACPDU packets in flight. + testhelper.AssignPortsToAggregateID(t, host, portChannel, peerPorts[0].Host, peerPorts[1].Host, peerPorts[2].Host, peerPorts[3].Host) + testhelper.AssignPortsToAggregateID(t, peer, portChannel, peerPorts[0].Peer, peerPorts[1].Peer, peerPorts[2].Peer, peerPorts[3].Peer) + + // Wait for the PortChannel to become active on each device. Then because LACPDU packets are used + // to notify peers about any state changes we sleep for a few seconds to give things time to + // converge. + gnmi.Await(t, host, gnmi.OC().Interface(portChannel).Enabled().State(), defaultGNMIWait, true) + gnmi.Await(t, peer, gnmi.OC().Interface(portChannel).Enabled().State(), defaultGNMIWait, true) + time.Sleep(3 * time.Second) + + // Choose a random port to test, and get the LACPDU count. + peerportslen := len(peerPorts) + max := big.NewInt(int64(peerportslen)) + randomIndex, _ := rand.Int(rand.Reader, max) + port_64 := randomIndex.Int64() + port := int(port_64) + hostBefore := gnmi.Get(t, host, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[port].Host).Counters().State()) + peerBefore := gnmi.Get(t, peer, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[port].Peer).Counters().State()) + + // Then sleep for a minute and get the count again. + time.Sleep(time.Minute) + hostAfter := gnmi.Get(t, host, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[port].Host).Counters().State()) + peerAfter := gnmi.Get(t, peer, gnmi.OC().Lacp().Interface(portChannel).Member(peerPorts[port].Peer).Counters().State()) + + // Finally, verify that the total number of LACPDU packets is acceptable for that 1 minute range. + hostCount := hostAfter.GetLacpInPkts() - hostBefore.GetLacpInPkts() + if err := acceptableLACPDUPacketCountForOneMinute(hostPeriod, hostCount); err != nil { + t.Errorf("Host LACPDU count is unacceptable for %v:%v: %v", host.Name(), peerPorts[port].Host, err) + } + peerCount := peerAfter.GetLacpInPkts() - peerBefore.GetLacpInPkts() + if err := acceptableLACPDUPacketCountForOneMinute(peerPeriod, peerCount); err != nil { + t.Errorf("Peer LACPDU count is unacceptable for %v:%v: %v", peer.Name(), peerPorts[port].Peer, err) + } + + // Also do a sanity check that gNMI is reporting the LacpOutPkts. Assuming we get here without + // failure then we know LACP is sending the packets out so we don't really care what this value is + // so long at it's >0. + if outPackets := hostAfter.GetLacpOutPkts(); outPackets == 0 { + t.Errorf("Host is not reporting any LACPDU output packets: got=%v", outPackets) + } + + return nil +} + +func TestLACPTimeouts(t *testing.T) { + // Testing LACPDU behavior with different timeout & activity settings (b4feaa45) and + // LACPDU counters (e9805bdf). + defer testhelper.NewTearDownOptions(t).WithID("b4feaa45-6088-4fa5-9f62-8adbc933c693").WithID("e9805bdf-1349-4fec-940b-1c710dc0c849").Teardown(t) + + tests := []struct { + hostActivity oc.E_Lacp_LacpActivityType + hostPeriod oc.E_Lacp_LacpPeriodType + peerActivity oc.E_Lacp_LacpActivityType + peerPeriod oc.E_Lacp_LacpPeriodType + }{ + {oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_FAST, oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_FAST}, + {oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_FAST, oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_SLOW}, + {oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_FAST, oc.Lacp_LacpActivityType_PASSIVE, oc.Lacp_LacpPeriodType_FAST}, + {oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_FAST, oc.Lacp_LacpActivityType_PASSIVE, oc.Lacp_LacpPeriodType_SLOW}, + {oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_SLOW, oc.Lacp_LacpActivityType_PASSIVE, oc.Lacp_LacpPeriodType_FAST}, + {oc.Lacp_LacpActivityType_ACTIVE, oc.Lacp_LacpPeriodType_SLOW, oc.Lacp_LacpActivityType_PASSIVE, oc.Lacp_LacpPeriodType_SLOW}, + } + + for _, test := range tests { + // Pretty print the test name based on the activity & period settings for host and peer switches. + // The names should look like: ActiveFastWithPassiveSlow, ActiveFastWithActiveSlow, etc. + hostSettings := lacpActivityTypeToString(test.hostActivity) + lacpPeriodTypeToString(test.hostPeriod) + peerSettings := lacpActivityTypeToString(test.peerActivity) + lacpPeriodTypeToString(test.peerPeriod) + name := hostSettings + "With" + peerSettings + + t.Run(name, func(t *testing.T) { + if got := verifyLACPTimeout(t, test.hostActivity, test.hostPeriod, test.peerActivity, test.peerPeriod); got != nil { + t.Errorf("LACP timeout test failed: %v", got) + } + }) + } +} + +// Used by go/ondatra to automatically reserve an available testbed. +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} diff --git a/tests/link_event_damping_test.go b/tests/link_event_damping_test.go new file mode 100644 index 0000000..f9cc168 --- /dev/null +++ b/tests/link_event_damping_test.go @@ -0,0 +1,436 @@ +package link_event_damping_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + "google.golang.org/grpc" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygnmi/ygnmi" +) + +const ( + holdTimeDisableMs = uint32(0) + holdTimeEnableMs = uint32(1000) + loopbackModeEnabled = oc.Interfaces_LoopbackModeType_FACILITY + waitAFterLinkEventDampingDisable = 1 * time.Second // Wait time after disabling damping config to ensure damped event got advertised and updated in DB. + waitAfterLoopbackModeChange = 10 * time.Second // Maximum time for link to flap and get updated in state DB after loopback-mode change. + configTimeout = 5 * time.Second // Maximum allowed time for a config to get programmed. + upTransitionNotificationTimeout = 3 * time.Second // Maximum allowed time for test to receive link up event notification on admin enable operation. + downTransitionNotificationTimeout = 3 * time.Second // Maximum allowed time for test to receive link down event notification on admin disable operation. +) + +func setLinkEventDampingConfig(t *testing.T, dut *ondatra.DUTDevice, intf string, holdTimeUp uint32) { + t.Helper() + // Configure the link event damping config. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).HoldTime().Up().Config(), holdTimeUp) + gnmi.Await(t, dut, gnmi.OC().Interface(intf).HoldTime().Up().State(), configTimeout, holdTimeUp) +} + +func fetchLinkEventDampingConfig(t *testing.T, dut *ondatra.DUTDevice, intf string) uint32 { + t.Helper() + // Lookup the hold time UP state value. + holdTimeUp, present := gnmi.Lookup(t, dut, gnmi.OC().Interface(intf).HoldTime().Up().State()).Val() + if present { + return holdTimeUp + } + return 0 +} + +func setLoopbackMode(t *testing.T, dut *ondatra.DUTDevice, intf string, loopbackMode oc.E_Interfaces_LoopbackModeType) { + t.Helper() + // Configure the loopback-mode. + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).LoopbackMode().Config(), loopbackMode) + gnmi.Await(t, dut.GNMIOpts().WithYGNMIOpts(ygnmi.WithSubscriptionMode(gpb.SubscriptionMode_ON_CHANGE)), gnmi.OC().Interface(intf).LoopbackMode().State(), configTimeout, loopbackMode) +} + +func fetchLoopbackMode(t *testing.T, dut *ondatra.DUTDevice, intf string) oc.E_Interfaces_LoopbackModeType { + t.Helper() + // Lookup the loopback mode state value. + loopbackMode, present := gnmi.Lookup(t, dut, gnmi.OC().Interface(intf).LoopbackMode().State()).Val() + if !present { + return oc.Interfaces_LoopbackModeType_NONE + } + return loopbackMode +} + +func restoreConfig(t *testing.T, dut *ondatra.DUTDevice, control *ondatra.DUTDevice, intf string, controlIntf string, loopbackModeDut oc.E_Interfaces_LoopbackModeType, + holdTimeUpDut uint32, holdTimeUpControl uint32) { + t.Helper() + + setLoopbackMode(t, dut, intf, loopbackModeDut) + time.Sleep(waitAfterLoopbackModeChange) + + var dutIntfNotUp bool = false + var controlIntfNotUp bool = false + // Verify port is UP after test. + if operStatus := gnmi.Get(t, dut, gnmi.OC().Interface(intf).OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + t.Errorf("DUT: got %v but want %v.", operStatus, oc.Interface_OperStatus_UP) + dutIntfNotUp = true + } + if operStatus := gnmi.Get(t, control, gnmi.OC().Interface(controlIntf).OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + t.Errorf("Control switch: got %v but want %v.", operStatus, oc.Interface_OperStatus_UP) + controlIntfNotUp = true + } + + // If port is not UP, try to bring it UP. + if dutIntfNotUp || controlIntfNotUp { + if dutIntfNotUp { + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), true) + } + if controlIntfNotUp { + gnmi.Replace(t, control, gnmi.OC().Interface(controlIntf).Enabled().Config(), true) + } + maxTimeForPortToComeUp := 15 * time.Second + if err := testhelper.WaitForInterfaceState(t, dut, intf, oc.Interface_OperStatus_UP, maxTimeForPortToComeUp); err != nil { + t.Errorf("DUT: failed to bring port UP: %v", err) + } + if err := testhelper.WaitForInterfaceState(t, dut, intf, oc.Interface_OperStatus_UP, maxTimeForPortToComeUp); err != nil { + t.Errorf("Control switch: failed to bring port UP: %v", err) + } + } + + setLinkEventDampingConfig(t, dut, intf, holdTimeUpDut) + setLinkEventDampingConfig(t, control, controlIntf, holdTimeUpControl) +} + +// Flaps a port N times and collects the port oper status change notifications. +func flapPortAndCollectOperStatusNotifications(t *testing.T, dut *ondatra.DUTDevice, intf string, numberOfFlaps int, + verifyStateAfterOp bool, timeout time.Duration) ([]*ygnmi.Value[oc.E_Interface_OperStatus], error) { + t.Helper() + + var operStatusSamples []*ygnmi.Value[oc.E_Interface_OperStatus] + var failed bool = false + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(context.Background(), grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + operStatusSamples = gnmi.Collect(t, dut.GNMIOpts().WithClient(gnmiClient). + WithYGNMIOpts(ygnmi.WithSubscriptionMode(gpb.SubscriptionMode_ON_CHANGE)), gnmi.OC().Interface(intf). + OperStatus().State(), timeout).Await(t) + // One extra sample is always received during collection - sample received + // immediately after subscription, so remove that sample from the list. + if len(operStatusSamples) >= 1 { + operStatusSamples = operStatusSamples[1:] + } + t.Logf("Successfully got ON_CHANGE oper status sample: %v", operStatusSamples) + }() + + // Wait for ON_CHANGE collect request to be sent before flapping the port. + time.Sleep(2 * time.Second) + for i := 0; i < numberOfFlaps; i++ { + t.Logf("Flap count: %v", i+1) + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), false) + if verifyStateAfterOp { + if err := testhelper.WaitForInterfaceState(t, dut, intf, oc.Interface_OperStatus_DOWN, downTransitionNotificationTimeout); err != nil { + t.Errorf("%v", err) + failed = true + } + } else { + time.Sleep(downTransitionNotificationTimeout) + } + gnmi.Replace(t, dut, gnmi.OC().Interface(intf).Enabled().Config(), true) + if verifyStateAfterOp { + if err := testhelper.WaitForInterfaceState(t, dut, intf, oc.Interface_OperStatus_UP, upTransitionNotificationTimeout); err != nil { + t.Errorf("%v", err) + failed = true + } + } else { + time.Sleep(upTransitionNotificationTimeout) + } + } + + // Wait for oper status sample collection go routine to complete before + // verifying the sample result. + wg.Wait() + + if failed == true { + return nil, errors.Errorf("Verify state failed after port operation.") + } + return operStatusSamples, nil +} + +func isLinkUp(t *testing.T, dut *ondatra.DUTDevice, control *ondatra.DUTDevice, intf string, controlIntf string) error { + t.Helper() + + // Check port is UP. + if operStatus := gnmi.Get(t, dut, gnmi.OC().Interface(intf).OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + return errors.Errorf("DUT: got %v oper status but want %v.", operStatus, oc.Interface_OperStatus_UP) + } + if operStatus := gnmi.Get(t, control, gnmi.OC().Interface(controlIntf).OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + return errors.Errorf("control switch: got %v oper status but want %v.", operStatus, oc.Interface_OperStatus_UP) + } + return nil +} + +// Returns a port to run the test. +func selectPortToRunTest(t *testing.T, dut *ondatra.DUTDevice, control *ondatra.DUTDevice) (string, string, error) { + t.Helper() + + var portList []string + for _, port := range dut.Ports() { + portList = append(portList, port.Name()) + } + + dutIntf, err := testhelper.RandomInterface(t, dut, &testhelper.RandomInterfaceParams{PortList: portList}) + if err != nil { + return "", "", errors.Errorf("RandomInterface failed to get an UP interface on DUT: %v", err) + } + for _, port := range dut.Ports() { + if port.Name() == dutIntf { + if control.Port(t, port.ID()) == nil { + return "", "", errors.Errorf("control interface for DUT interface %v not found", dutIntf) + } + return dutIntf, control.Port(t, port.ID()).Name(), nil + } + } + return "", "", errors.Errorf("control interface for DUT interface %v not found", dutIntf) +} + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// Disables the link event damping config on a link and does N flaps on the +// interface and verifies that link events are not damped. +func TestLinkEventDampingConfigDisabled(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("396d27cb-f951-4acf-8cec-98b17a9a5175").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + control := ondatra.DUT(t, "CONTROL") + // Select a random UP interface. + intf, controlIntf, err := selectPortToRunTest(t, dut, control) + if err != nil { + t.Fatalf("selectPortToRunTest() failed to get a port to run test: %v", err) + } + t.Logf("Running test on DUT interface: %v, control interface: %v.", intf, controlIntf) + + err = isLinkUp(t, dut, control, intf, controlIntf) + if err != nil { + t.Fatalf("isLinkUp() failed: %v", err) + } + + // Save the current config on port. + oldHoldTimeUpDut := fetchLinkEventDampingConfig(t, dut, intf) + t.Logf("Initial hold-time up config on DUT interface: %v", oldHoldTimeUpDut) + oldLoopbackModeDut := fetchLoopbackMode(t, dut, intf) + t.Logf("Initial loopback-mode on DUT interface: %v", oldLoopbackModeDut) + oldHoldTimeUpControl := fetchLinkEventDampingConfig(t, control, controlIntf) + t.Logf("Initial hold-time up config on control interface: %v", oldHoldTimeUpControl) + + // Restore the config after the test. + t.Cleanup(func() { + restoreConfig(t, dut, control, intf, controlIntf, oldLoopbackModeDut, oldHoldTimeUpDut, oldHoldTimeUpControl) + }) + + // Disable link event damping. + setLinkEventDampingConfig(t, dut, intf, holdTimeDisableMs) + setLinkEventDampingConfig(t, control, controlIntf, holdTimeDisableMs) + time.Sleep(waitAFterLinkEventDampingDisable) + // Set MAC loopback on port. + setLoopbackMode(t, dut, intf, loopbackModeEnabled) + time.Sleep(waitAfterLoopbackModeChange) + // Get initial carrier transitions. + initCarrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()) + t.Logf("Initial carrier transitions: %v", initCarrierTransitions) + + numberOfFlaps := 10 + // Flap the port and collect oper status samples. + collectTimeout := time.Duration(numberOfFlaps)*(upTransitionNotificationTimeout+downTransitionNotificationTimeout) + 5*time.Second + operStatusSamples, flapError := flapPortAndCollectOperStatusNotifications(t, dut, intf, numberOfFlaps, true, collectTimeout) + if flapError != nil { + t.Errorf("flapPortAndCollectOperStatusNotifications failed on port: %v", flapError) + } else if len(operStatusSamples) != 2*numberOfFlaps { + t.Errorf("flapPortAndCollectOperStatusNotifications got %v samples, want %v samples.", len(operStatusSamples), 2*numberOfFlaps) + } + finalCarrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()) + t.Logf("Final carrier transitions: %v", finalCarrierTransitions) + + // Verify the events count. + carrierTransitions := finalCarrierTransitions - initCarrierTransitions + if carrierTransitions != uint64(2*numberOfFlaps) { + t.Errorf("Got: %v carrier transitions but want: %v", carrierTransitions, 2*numberOfFlaps) + } +} + +// Tests that first link flap events on an interface is not damped after +// enabling the link event damping config. +func TestFirstFlapEventsNotDampedAfterLinkEventDampingConfig(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("a92fd0c4-0461-4379-a8ba-76ce6710d8a4").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + control := ondatra.DUT(t, "CONTROL") + intf, controlIntf, err := selectPortToRunTest(t, dut, control) + if err != nil { + t.Fatalf("selectPortToRunTest() failed to get a port to run test: %v", err) + } + t.Logf("Running test on DUT interface: %v, control interface: %v.", intf, controlIntf) + + err = isLinkUp(t, dut, control, intf, controlIntf) + if err != nil { + t.Fatalf("isLinkUp() failed: %v", err) + } + + // Save the current config on port. + oldHoldTimeUpDut := fetchLinkEventDampingConfig(t, dut, intf) + t.Logf("Initial hold-time up config on DUT interface: %v", oldHoldTimeUpDut) + oldLoopbackModeDut := fetchLoopbackMode(t, dut, intf) + t.Logf("Initial loopback-mode on DUT interface: %v", oldLoopbackModeDut) + oldHoldTimeUpControl := fetchLinkEventDampingConfig(t, control, controlIntf) + t.Logf("Initial hold-time up config on control interface: %v", oldHoldTimeUpControl) + + // Restore the config after the test. + t.Cleanup(func() { + // Disable link event damping config on port to clear the damped state + // before restoring the config. + setLinkEventDampingConfig(t, dut, intf, holdTimeDisableMs) + time.Sleep(waitAFterLinkEventDampingDisable) + restoreConfig(t, dut, control, intf, controlIntf, oldLoopbackModeDut, oldHoldTimeUpDut, oldHoldTimeUpControl) + }) + + // Disable link event damping so that damped state is cleared if present. + setLinkEventDampingConfig(t, dut, intf, holdTimeDisableMs) + setLinkEventDampingConfig(t, control, controlIntf, holdTimeDisableMs) + time.Sleep(waitAFterLinkEventDampingDisable) + // Set MAC loopback on port. + setLoopbackMode(t, dut, intf, loopbackModeEnabled) + time.Sleep(waitAfterLoopbackModeChange) + // Enable link event damping on DUT. + setLinkEventDampingConfig(t, dut, intf, holdTimeEnableMs) + // Get initial carrier transitions. + initCarrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()) + t.Logf("Initial carrier transitions: %v", initCarrierTransitions) + + // Flap the port and events should not be damped and notified immediately. + numberOfFlaps := 1 + collectTimeout := time.Duration(numberOfFlaps)*(upTransitionNotificationTimeout+downTransitionNotificationTimeout) + 5*time.Second + operStatusSamples, flapError := flapPortAndCollectOperStatusNotifications(t, dut, intf, numberOfFlaps, true, collectTimeout) + if flapError != nil { + t.Errorf("flapPortAndCollectOperStatusNotifications failed on port: %v", flapError) + } else if len(operStatusSamples) != 2 { + t.Errorf("flapPortAndCollectOperStatusNotifications got %v samples, want 2 samples.", len(operStatusSamples)) + } + if carrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()); carrierTransitions != initCarrierTransitions+2 { + t.Errorf("Got: %v carrier transitions but want: %v", carrierTransitions, initCarrierTransitions+2) + } +} + +// Tests that when multiple flaps happen one after another on a port, port +// remains damped. +func TestMultipleFlapsWithLinkEventDampingConfig(t *testing.T) { + // Report results to TestTracker at the end. + defer testhelper.NewTearDownOptions(t).WithID("5ccd7b33-8661-4b82-9d19-971816b2aeea").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + control := ondatra.DUT(t, "CONTROL") + intf, controlIntf, err := selectPortToRunTest(t, dut, control) + if err != nil { + t.Fatalf("selectPortToRunTest() failed to get a port to run test: %v", err) + } + t.Logf("Running test on DUT interface: %v, control interface: %v.", intf, controlIntf) + + err = isLinkUp(t, dut, control, intf, controlIntf) + if err != nil { + t.Fatalf("isLinkUp() failed: %v", err) + } + + // Save the current config on port. + oldHoldTimeUpDut := fetchLinkEventDampingConfig(t, dut, intf) + t.Logf("Initial hold-time up config on DUT interface: %v", oldHoldTimeUpDut) + oldLoopbackModeDut := fetchLoopbackMode(t, dut, intf) + t.Logf("Initial loopback-mode on DUT interface: %v", oldLoopbackModeDut) + oldHoldTimeUpControl := fetchLinkEventDampingConfig(t, control, controlIntf) + t.Logf("Initial hold-time up config on control interface: %v", oldHoldTimeUpControl) + + // Restore the config after the test. + t.Cleanup(func() { + // Disable link event damping config on port to clear the damped state + // before restoring the config. + setLinkEventDampingConfig(t, dut, intf, holdTimeDisableMs) + time.Sleep(waitAFterLinkEventDampingDisable) + restoreConfig(t, dut, control, intf, controlIntf, oldLoopbackModeDut, oldHoldTimeUpDut, oldHoldTimeUpControl) + }) + + // Disable link event damping so that damped state is cleared if present. + setLinkEventDampingConfig(t, dut, intf, holdTimeDisableMs) + setLinkEventDampingConfig(t, control, controlIntf, holdTimeDisableMs) + time.Sleep(waitAFterLinkEventDampingDisable) + // Set MAC loopback on port. + setLoopbackMode(t, dut, intf, loopbackModeEnabled) + time.Sleep(waitAfterLoopbackModeChange) + // Enable link event damping. + setLinkEventDampingConfig(t, dut, intf, holdTimeEnableMs) + // Get initial carrier transitions. + initCarrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()) + t.Logf("Initial carrier transitions: %v", initCarrierTransitions) + + // First flap events should not be damped after config enable and notified + // immediately. + numberOfFlaps := 1 + collectTimeout := time.Duration(numberOfFlaps)*(upTransitionNotificationTimeout+downTransitionNotificationTimeout) + 5*time.Second + operStatusSamples, flapError := flapPortAndCollectOperStatusNotifications(t, dut, intf, numberOfFlaps, true, collectTimeout) + if flapError != nil { + t.Errorf("flapPortAndCollectOperStatusNotifications failed to verify first flap on port: %v", flapError) + } else if len(operStatusSamples) != 2 { + t.Errorf("flapPortAndCollectOperStatusNotifications got %v samples, but want 2 samples.", len(operStatusSamples)) + } + if carrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()); carrierTransitions != initCarrierTransitions+2 { + t.Errorf("Got: %v carrier transitions but want: %v", carrierTransitions, initCarrierTransitions+2) + } + // DOWN event of second flap should start the damping and DOWN event + // notification should be observed but UP event will be damped. + operStatusSamples, flapError = flapPortAndCollectOperStatusNotifications(t, dut, intf, numberOfFlaps, false, collectTimeout) + if flapError != nil { + t.Errorf("flapPortAndCollectOperStatusNotifications failed on port: %v", flapError) + } else if len(operStatusSamples) != 1 { + t.Errorf("flapPortAndCollectOperStatusNotifications got %v samples, want 1 sample.", len(operStatusSamples)) + } + if carrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()); carrierTransitions != initCarrierTransitions+3 { + t.Errorf("Got: %v carrier transitions but want: %v", carrierTransitions, initCarrierTransitions+3) + } + if operStatus := gnmi.Get(t, dut, gnmi.OC().Interface(intf).OperStatus().State()); operStatus != oc.Interface_OperStatus_DOWN { + t.Errorf("Got %v oper status but want %v.", operStatus, oc.Interface_OperStatus_DOWN) + } + // Subsequent flap events should be damped. + numberOfFlaps = 10 + collectTimeout = time.Duration(numberOfFlaps)*(upTransitionNotificationTimeout+downTransitionNotificationTimeout) + 30*time.Second + operStatusSamples, flapError = flapPortAndCollectOperStatusNotifications(t, dut, intf, numberOfFlaps, false, collectTimeout) + if flapError != nil { + t.Errorf("flapPortAndCollectOperStatusNotifications failed on port: %v", flapError) + } else if len(operStatusSamples) != 0 { + t.Errorf("flapPortAndCollectOperStatusNotifications got %v samples, want 0 sample.", len(operStatusSamples)) + } + if carrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()); carrierTransitions != initCarrierTransitions+3 { + t.Errorf("Got: %v carrier transitions but want: %v", carrierTransitions, initCarrierTransitions+3) + } + if operStatus := gnmi.Get(t, dut, gnmi.OC().Interface(intf).OperStatus().State()); operStatus != oc.Interface_OperStatus_DOWN { + t.Errorf("Got %v oper status but want %v.", operStatus, oc.Interface_OperStatus_DOWN) + } + // Disable link event damping so that damped state is cleared and UP + // notification is received. + setLinkEventDampingConfig(t, dut, intf, holdTimeDisableMs) + time.Sleep(waitAFterLinkEventDampingDisable) + + if carrierTransitions := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Counters().CarrierTransitions().State()); carrierTransitions != initCarrierTransitions+4 { + t.Errorf("Got: %v carrier transitions but want: %v", carrierTransitions, initCarrierTransitions+4) + } + if operStatus := gnmi.Get(t, dut, gnmi.OC().Interface(intf).OperStatus().State()); operStatus != oc.Interface_OperStatus_UP { + t.Errorf("Got %v oper status but want %v.", operStatus, oc.Interface_OperStatus_UP) + } +} diff --git a/tests/mgmt_interface_test.go b/tests/mgmt_interface_test.go new file mode 100644 index 0000000..e9bca61 --- /dev/null +++ b/tests/mgmt_interface_test.go @@ -0,0 +1,594 @@ +package mgmt_interface_test + +// This suite of tests exercises the gNMI paths associated with the management + +import ( + "errors" + "fmt" + "math/rand" + "net" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/testt" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +var ( + bond0Name = "bond0" + interfaceIndex = uint32(0) + calledMockConfigPush = false + managementInterfaces = []string{ + bond0Name, + } +) + +type ipAddressInfo struct { + address string + prefixLength uint8 +} + +func fetchMgmtIPv4AddressAndPrefix(t *testing.T) (ipAddressInfo, error) { + // Reads the existing management interface IPv4 address. For these tests, + // we will not be able to change the address without breaking connection of + // the proxy used by the test. + dut := ondatra.DUT(t, "DUT") + ipInfo := gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv4().State()) + for _, v := range ipInfo.Address { + addr := v.GetIp() + if addr == "" { + continue + } + return ipAddressInfo{address: addr, prefixLength: v.GetPrefixLength()}, nil + } + return ipAddressInfo{}, errors.New("no IPv4 management interface has been configured") +} + +func fetchMgmtIPv6AddressAndPrefix(t *testing.T) (ipAddressInfo, error) { + // Reads the existing management interface IPv6 address. For these tests, + // we will not be able to change the address without breaking connection of + // the proxy used by the test. + dut := ondatra.DUT(t, "DUT") + ipInfo := gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().State()) + for _, v := range ipInfo.Address { + addr := v.GetIp() + if addr == "" { + continue + } + return ipAddressInfo{address: addr, prefixLength: v.GetPrefixLength()}, nil + } + return ipAddressInfo{}, errors.New("no IPv6 management interface has been configured") +} + +func mockConfigPush(t *testing.T) { + // Performs a mock config push by ensuring the management interface database + // entries expected for IPv4 and IPv6 addresses have been setup. + // TODO: Remove calls to this function once the helper function + // to perform a default config during setup is available. + dut := ondatra.DUT(t, "DUT") + d := &oc.Root{} + + // Create the bond0 interface. + if !calledMockConfigPush { + t.Logf("Config push for %v", bond0Name) + newIface := d.GetOrCreateInterface(bond0Name) + newIface.Name = &bond0Name + newIface.Type = oc.IETFInterfaces_InterfaceType_ieee8023adLag + gnmi.Replace(t, dut, gnmi.OC().Interface(bond0Name).Config(), newIface) + calledMockConfigPush = true + } +} + +// ----------------------------------------------------------------------------- +// Generic management interface path tests +// ----------------------------------------------------------------------------- + +func TestGetInterfaceDefaultInfo(t *testing.T) { + // This test confirms generic management interface information is correct. + // Paths tested: + // /interfaces/interface[name=]/ethernet/state/mac-address + // /interfaces/interface[name=]/state/name + // /interfaces/interface[name=]/state/oper-status + // /interfaces/interface[name=]/state/type + defer testhelper.NewTearDownOptions(t).WithID("1b0707dd-4112-4c0f-ad74-1998df876747").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + for _, iface := range managementInterfaces { + mgmtInterface := gnmi.OC().Interface(iface) + macAddress := gnmi.Get(t, dut, mgmtInterface.Ethernet().MacAddress().State()) + if _, err := net.ParseMAC(macAddress); err != nil { + t.Errorf("MGMT component (%v) has invalid mac-address format! got:%v: %v", iface, macAddress, err) + } + if mgmtName := gnmi.Get(t, dut, mgmtInterface.Name().State()); mgmtName != iface { + t.Errorf("MGMT component (%v) name match failed! got:%v, want:%v", iface, mgmtName, iface) + } + if operStatus, statusWant := gnmi.Get(t, dut, mgmtInterface.OperStatus().State()), oc.Interface_OperStatus_UP; operStatus != statusWant { + t.Errorf("MGMT component (%v) oper-status match failed! got:%v, want:%v", iface, operStatus, statusWant) + } + if ifaceType, typeWant := gnmi.Get(t, dut, mgmtInterface.Type().State()), oc.IETFInterfaces_InterfaceType_ieee8023adLag; ifaceType != typeWant { + t.Errorf("MGMT component (%v) type match failed! got:%v, want:%v", iface, ifaceType, typeWant) + } + } +} + +func TestSetName(t *testing.T) { + // This test confirms the name of a management interface can be written. + // Paths tested: + // /interfaces/interface[name=]/config/name + // /interfaces/interface[name=]/state/name + defer testhelper.NewTearDownOptions(t).WithID("826882ed-0534-499c-880a-91cb3c078a03").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + mgmtInterfaceState := gnmi.OC().Interface(bond0Name) + mgmtInterfaceConfig := gnmi.OC().Interface(bond0Name) + + gnmi.Replace(t, dut, mgmtInterfaceConfig.Name().Config(), bond0Name) + gnmi.Await(t, dut, mgmtInterfaceState.Name().State(), 5*time.Second, bond0Name) + + // Expect name on state path to have changed. + if configuredName := gnmi.Get(t, dut, mgmtInterfaceConfig.Name().Config()); configuredName != bond0Name { + t.Errorf("MGMT component (%v) name match failed! set:%v, config-path-value:%v (want:%v)", bond0Name, bond0Name, configuredName, bond0Name) + } +} + +func TestSetInvalidType(t *testing.T) { + // This test confirms that an invalid type cannot be set for the management + // interface. + // Paths tested: + // /interfaces/interface[name=]/config/type + // /interfaces/interface[name=]/state/type + defer testhelper.NewTearDownOptions(t).WithID("0c838fb4-6846-4f5e-a5db-cbcabacdb020").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + for _, iface := range managementInterfaces { + mgmtInterfaceState := gnmi.OC().Interface(iface) + mgmtInterfaceConfig := gnmi.OC().Interface(iface) + originalType := gnmi.Get(t, dut, mgmtInterfaceState.Type().State()) + originalConfigType := gnmi.Get(t, dut, mgmtInterfaceConfig.Type().Config()) + + invalidType := oc.IETFInterfaces_InterfaceType_softwareLoopback + // This call should fail, since the type is invalid for a management interface. + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, mgmtInterfaceConfig.Type().Config(), invalidType) + }) + stateType := gnmi.Get(t, dut, mgmtInterfaceState.Type().State()) + configuredType := gnmi.Get(t, dut, mgmtInterfaceConfig.Type().Config()) + + // Invalid type should not have gone through. + if stateType != originalType || configuredType == invalidType { + t.Errorf("MGMT component (%v) type match failed! set:%v, config-path-value:%v (want:%v), state-path-value:%v (want:%v)", iface, invalidType, configuredType, originalConfigType, stateType, originalType) + } + } +} + +func TestSetInvalidName(t *testing.T) { + // This test confirms that an invalid name cannot be set for the management + // interface. + // Paths tested: + // /interfaces/interface[name=]/config/name + // /interfaces/interface[name=]/state/name + defer testhelper.NewTearDownOptions(t).WithID("67a8c951-34cc-4148-9f09-a779e7976d03").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + mgmtInterfaceState := gnmi.OC().Interface(bond0Name) + mgmtInterfaceConfig := gnmi.OC().Interface(bond0Name) + originalName := gnmi.Get(t, dut, mgmtInterfaceState.Name().State()) + originalConfigName := gnmi.Get(t, dut, mgmtInterfaceConfig.Name().Config()) + + invalidName := "mybond0" + // Setting invalid name should be ignored. + // TODO: This replace call should fail. + gnmi.Replace(t, dut, mgmtInterfaceConfig.Name().Config(), invalidName) + gnmi.Await(t, dut, mgmtInterfaceState.Name().State(), 5*time.Second, originalName) + + // Invalid name should not be accepted. + if configuredName := gnmi.Get(t, dut, mgmtInterfaceConfig.Name().Config()); configuredName == invalidName { + t.Errorf("MGMT component (%v) name match failed! set:%v, config-path-value:%v (want:%v)", bond0Name, invalidName, configuredName, originalConfigName) + } +} + +// ----------------------------------------------------------------------------- +// Counter path tests +// ----------------------------------------------------------------------------- + +func verifyInCounters(counters *oc.Interface_Counters) []error { + var rv []error + if counters.InDiscards == nil { + rv = append(rv, errors.New("in-discards")) + } + if counters.InErrors == nil { + rv = append(rv, errors.New("in-errors")) + } + if counters.InOctets == nil { + rv = append(rv, errors.New("in-octets")) + } + if counters.InPkts == nil { + rv = append(rv, errors.New("in-pkts")) + } + return rv +} + +func verifyOutCounters(counters *oc.Interface_Counters) []error { + var rv []error + if counters.OutDiscards == nil { + rv = append(rv, errors.New("out-discards")) + } + if counters.OutErrors == nil { + rv = append(rv, errors.New("out-errors")) + } + if counters.OutOctets == nil { + rv = append(rv, errors.New("out-octets")) + } + if counters.OutPkts == nil { + rv = append(rv, errors.New("out-pkts")) + } + return rv +} + +func TestInCounters(t *testing.T) { + // This test confirms that the input counters (RX side) are updated by packet + // events. Note: the management interface is the connection by which gNMI + // operations take place, so it is difficult to get a precise count of the + // expected differences in counter values. + // Paths tested: + // /interfaces/interface[name=]/state/counters/in-discards + // /interfaces/interface[name=]/state/counters/in-errors + // /interfaces/interface[name=]/state/counters/in-octets + // /interfaces/interface[name=]/state/counters/in-pkts + defer testhelper.NewTearDownOptions(t).WithID("b0801966-fb60-456d-9c91-ee3191c5e7e1").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + counters := gnmi.OC().Interface(bond0Name).Counters() + initialState := gnmi.Get(t, dut, counters.State()) + if errors := verifyInCounters(initialState); len(errors) != 0 { + t.Fatalf("MGMT component (%v) has invalid initial input counters: %v", bond0Name, errors) + } + + t.Logf("Initial in-counters state has:") + t.Logf(" in-discards:%v in-errors:%v in-octets:%v in-pkts: %v", initialState.GetInDiscards(), initialState.GetInErrors(), initialState.GetInOctets(), initialState.GetInPkts()) + + // The management interface is active. That is how gNMI operations are + // communicated to the switch. In this test, we verify that packets have been + // received and that there were no errors. + // Initiate a handful of Get operations to ensure there is traffic. + gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Name().State()) + gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Name().State()) + gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Name().State()) + + nextState := gnmi.Get(t, dut, counters.State()) + if errors := verifyInCounters(nextState); len(errors) != 0 { + t.Fatalf("MGMT component (%v) has invalid next input counters: %v", bond0Name, errors) + } + + t.Logf("Next in-counters state has:") + t.Logf(" in-discards:%v in-errors:%v in-octets:%v in-pkts: %v", nextState.GetInDiscards(), nextState.GetInErrors(), nextState.GetInOctets(), nextState.GetInPkts()) + + if initialState.GetInDiscards() > nextState.GetInDiscards() { + t.Errorf("MGMT component (%v) has unexpected decrease in in-discards %v -> %v", bond0Name, initialState.GetInDiscards(), nextState.GetInDiscards()) + } + if nextState.GetInDiscards() != 0 { + t.Logf("MGMT component (%v) has non-zero in-discards: %v", bond0Name, nextState.GetInDiscards()) + } + if initialState.GetInErrors() > nextState.GetInErrors() { + t.Errorf("MGMT component (%v) has unexpected decrease in in-errors %v -> %v", bond0Name, initialState.GetInErrors(), nextState.GetInErrors()) + } + if nextState.GetInErrors() != 0 { + t.Logf("MGMT component (%v) has non-zero in-errors: %v", bond0Name, nextState.GetInErrors()) + } + if initialState.GetInOctets() >= nextState.GetInOctets() { + t.Errorf("MGMT component (%v) in-octets did not increase as expected %v -> %v", bond0Name, initialState.GetInOctets(), nextState.GetInOctets()) + } + if initialState.GetInPkts() >= nextState.GetInPkts() { + t.Errorf("MGMT component (%v) in-pkts did not increase as expected %v -> %v", bond0Name, initialState.GetInPkts(), nextState.GetInPkts()) + } +} + +func TestOutCounters(t *testing.T) { + // This test confirms that the output counters (TX side) are updated by packet + // events. Note: the management interface is the connection by which gNMI + // operations take place, so it is difficult to get a precise count of the + // expected differences in counter values. + // Paths tested: + // /interfaces/interface[name=]/state/counters/out-discards + // /interfaces/interface[name=]/state/counters/out-errors + // /interfaces/interface[name=]/state/counters/out-octets + // /interfaces/interface[name=]/state/counters/out-pkts + defer testhelper.NewTearDownOptions(t).WithID("ccec883e-0b87-4084-8861-77393460976b").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + counters := gnmi.OC().Interface(bond0Name).Counters() + initialState := gnmi.Get(t, dut, counters.State()) + if errors := verifyOutCounters(initialState); len(errors) != 0 { + t.Fatalf("MGMT component (%v) has invalid initial output counters: %v", bond0Name, errors) + } + + t.Logf("Initial out-counters state has:") + t.Logf(" out-discards:%v out-errors:%v out-octets:%v out-pkts: %v", initialState.GetOutDiscards(), initialState.GetOutErrors(), initialState.GetOutOctets(), initialState.GetOutPkts()) + + // The management interface is active. That is how gNMI operations are + // communicated to the switch. In this test, we verify that packets have been + // received and that there were no errors. + // Initiate a handful of Get operations to ensure there is traffic. + gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Name().State()) + gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Name().State()) + gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Name().State()) + + nextState := gnmi.Get(t, dut, counters.State()) + if errors := verifyOutCounters(nextState); len(errors) != 0 { + t.Fatalf("MGMT component (%v) has invalid next output counters: %v", bond0Name, errors) + } + + t.Logf("Next out-counters state has:") + t.Logf(" out-discards:%v out-errors:%v out-octets:%v out-pkts: %v", nextState.GetOutDiscards(), nextState.GetOutErrors(), nextState.GetOutOctets(), nextState.GetOutPkts()) + + if initialState.GetOutDiscards() > nextState.GetOutDiscards() { + t.Errorf("MGMT component (%v) has unexpected decrease in out-discards %v -> %v", bond0Name, initialState.GetOutDiscards(), nextState.GetOutDiscards()) + } + if nextState.GetOutDiscards() != 0 { + t.Logf("MGMT component (%v) has non-zero out-discards: %v", bond0Name, nextState.GetOutDiscards()) + } + if initialState.GetOutErrors() > nextState.GetOutErrors() { + t.Errorf("MGMT component (%v) has unexpected decrease in out-errors %v -> %v", bond0Name, initialState.GetOutErrors(), nextState.GetOutErrors()) + } + if nextState.GetOutErrors() != 0 { + t.Logf("MGMT component (%v) has non-zero out-errors: %v", bond0Name, nextState.GetOutErrors()) + } + if initialState.GetOutOctets() >= nextState.GetOutOctets() { + t.Errorf("MGMT component (%v) out-octets did not increase as expected %v -> %v", bond0Name, initialState.GetOutOctets(), nextState.GetOutOctets()) + } + if initialState.GetOutPkts() >= nextState.GetOutPkts() { + t.Errorf("MGMT component (%v) out-pkts did not increase as expected %v -> %v", bond0Name, initialState.GetOutPkts(), nextState.GetOutPkts()) + } +} + +// ----------------------------------------------------------------------------- +// IPv4 path tests +// ----------------------------------------------------------------------------- +func TestSetIPv4AddressAndPrefixLength(t *testing.T) { + // This test confirms that a new IPv4 address and prefix-length can be added. + // Note: the entire "tree" has to be added in one gNMI operation. (The IP and + // prefix length cannot be written separately.) + // formed. + // Paths tested: + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv4/addresses/address[ip=
]/config/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv4/addresses/address[ip=
]/config/prefix-length + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv4/addresses/address[ip=
]/state/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv4/addresses/address[ip=
]/state/prefix-length + defer testhelper.NewTearDownOptions(t).WithID("64003075-93a5-41b3-b962-74e9f36dde94").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + // We can't change the management interface IP address; the connection via the + // proxy would be lost. We can, however, write the existing value again. + newIPv4Info, err := fetchMgmtIPv4AddressAndPrefix(t) + restoreIPv4State := false + + if err != nil { + // If IPv4 is not used in the testbed, we can set a valid address. + t.Logf("Unable to fetch IPv4 management address: %v", err) + t.Logf("We will create an unused one.") + // Address is [16:126].[0:255].[0:255].[0:255]. + start, end := 16, 126 + firstPrefixPart1 := make([]int, end-start+1) + for i := range firstPrefixPart1 { + firstPrefixPart1[i] = i + start + } + start, end = 128, 223 + firstPrefixPart2 := make([]int, end-start+1) + for i := range firstPrefixPart2 { + firstPrefixPart2[i] = i + start + } + firstPrefix := append(firstPrefixPart1, firstPrefixPart2...) + + newAddr := fmt.Sprintf("%d.%d.%d.%d", firstPrefix[rand.Int()%len(firstPrefix)], rand.Intn(256), rand.Intn(256), rand.Intn(256)) + newPrefix := uint8(rand.Intn(27) + 5) // 5 to 31 + newIPv4Info = ipAddressInfo{address: newAddr, prefixLength: newPrefix} + restoreIPv4State = true + } + + d := &oc.Root{} + iface := d.GetOrCreateInterface(bond0Name).GetOrCreateSubinterface(interfaceIndex) + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(newIPv4Info.address) + newV4.Ip = &newIPv4Info.address + newV4.PrefixLength = &newIPv4Info.prefixLength + + ipv4 := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv4().Address(newIPv4Info.address) + gnmi.Replace(t, dut, gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv4().Address(newIPv4Info.address).Config(), newV4) + if restoreIPv4State { + defer gnmi.Delete(t, dut, gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv4().Address(newIPv4Info.address).Config()) + } + // Give the configuration a chance to become active. + time.Sleep(1 * time.Second) + + if observed := gnmi.Get(t, dut, ipv4.State()); observed.GetIp() != newIPv4Info.address || observed.GetPrefixLength() != newIPv4Info.prefixLength { + t.Errorf("MGMT component (%v) address match failed! state-path-value:%v/%v (want:%v/%v)", bond0Name, observed.GetIp(), observed.GetPrefixLength(), newIPv4Info.address, newIPv4Info.prefixLength) + } +} + +func TestSetIPv4InvalidAddress(t *testing.T) { + // This test confirms that an invalid IPv4 address cannot be set. + // IPv4 addresses that begin with 0 or 255 (e.g. 255.1.2.3 or 0.4.5.6) are + // considered invalid. + // Paths tested: + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv4/addresses/address[ip=
]/config/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv4/addresses/address[ip=
]/state/ip + defer testhelper.NewTearDownOptions(t).WithID("00acbce9-069e-43e1-a511-9b45bb3ad5b0").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + var invalidIPPaths = []string{ + fmt.Sprintf("255.%v.%v.%v", rand.Intn(256), rand.Intn(256), rand.Intn(256)), + fmt.Sprintf("0.%v.%v.%v", rand.Intn(256), rand.Intn(256), rand.Intn(256)), + } + configuredIPv4PrefixLength := uint8(16) + + for _, invalidIPPath := range invalidIPPaths { + + d := &oc.Root{} + iface := d.GetOrCreateInterface(bond0Name).GetOrCreateSubinterface(interfaceIndex) + newV4 := iface.GetOrCreateIpv4().GetOrCreateAddress(invalidIPPath) + newV4.Ip = &invalidIPPath + newV4.PrefixLength = &configuredIPv4PrefixLength + + ipv4 := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv4().Address(invalidIPPath) + ipv4Config := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv4().Address(invalidIPPath) + // Cannot write invalid IPv4 address. + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, ipv4Config.Config(), newV4) + }) + + // There should be no IP set with the invalid IPv4 address. + testt.ExpectFatal(t, func(t testing.TB) { + observedIP := gnmi.Get(t, dut, ipv4.Ip().State()) + t.Logf("MGMT component (%v) observed IPv4 address: %v.", bond0Name, observedIP) + }) + } +} + +// ----------------------------------------------------------------------------- +// IPv6 path tests +// ----------------------------------------------------------------------------- +func TestGetIPv6DefaultInfo(t *testing.T) { + // This test confirms that generic IPv6 information can be read and is well + // formed. + // Paths tested: + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/state/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/state/prefix-length + defer testhelper.NewTearDownOptions(t).WithID("5bc725a2-befe-4154-bd7d-d390c87dc4d8").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + configuredIPv6Info, err := fetchMgmtIPv6AddressAndPrefix(t) + if err != nil { + t.Fatalf("Unable to fetch IPv6 management address: %v", err) + } + ipv6 := gnmi.Get(t, dut, gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Info.address).State()) + + if *ipv6.PrefixLength >= 128 { + t.Errorf("MGMT component (%v) has an incorrect prefix-length: %v (want: [0:127]) on subinterface %v with IP %v", bond0Name, *ipv6.PrefixLength, interfaceIndex, configuredIPv6Info.address) + } + parsedIP := net.ParseIP(*ipv6.Ip) + if parsedIP == nil { + t.Fatalf("MGMT component (%v) has an incorrectly formatted IPv6 address: %v", bond0Name, *ipv6.Ip) + } + ipAsBytes := parsedIP.To16() + if ipAsBytes == nil { + t.Fatalf("MGMT component (%v) has an incorrectly formatted IPv6 address: %v could not be parsed", bond0Name, *ipv6.Ip) + } + if len(ipAsBytes) != 16 { + t.Fatalf("MGMT component (%v) IPv6 address is only %v bytes.", bond0Name, len(ipAsBytes)) + } +} + +func TestSetIPv6AddressAndPrefixLength(t *testing.T) { + // This test confirms that a new IPv6 address and prefix-length can be added. + // Note: the entire "tree" has to be added in one gNMI operation. (The IP and + // prefix length cannot be written separately.) + // formed. + // Paths tested: + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/config/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/config/prefix-length + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/state/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/state/prefix-length + defer testhelper.NewTearDownOptions(t).WithID("0f79f318-b0de-4352-a045-540aa1da94d4").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + // We can't change the management interface IP address; the connection via the + // proxy would be lost. We can, however, write the existing value again. + newIPInfo, err := fetchMgmtIPv6AddressAndPrefix(t) + if err != nil { + t.Fatalf("Unable to fetch IPv6 management address: %v", err) + } + + d := &oc.Root{} + iface := d.GetOrCreateInterface(bond0Name).GetOrCreateSubinterface(interfaceIndex) + newV6 := iface.GetOrCreateIpv6().GetOrCreateAddress(newIPInfo.address) + newV6.Ip = &newIPInfo.address + newV6.PrefixLength = &newIPInfo.prefixLength + + ipv6 := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(newIPInfo.address) + gnmi.Replace(t, dut, gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(newIPInfo.address).Config(), newV6) + // Give the configuration a chance to become active. + time.Sleep(1 * time.Second) + + if observed := gnmi.Get(t, dut, ipv6.State()); *observed.Ip != newIPInfo.address || *observed.PrefixLength != newIPInfo.prefixLength { + t.Errorf("MGMT component (%v) address match failed! state-path-value:%v/%v (want:%v/%v)", bond0Name, *observed.Ip, *observed.PrefixLength, newIPInfo.address, newIPInfo.prefixLength) + } +} + +func TestSetIPv6InvalidPrefixLength(t *testing.T) { + // This test confirms that an invalid IPv6 prefix-length cannot be set. + // Any prefix length in the range [0:128] is supported. + // Paths tested: + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/config/prefix-length + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/state/prefix-length + defer testhelper.NewTearDownOptions(t).WithID("7813ab28-1d8c-43ca-ab21-d4106a733e47").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + configuredIPv6Info, err := fetchMgmtIPv6AddressAndPrefix(t) + if err != nil { + t.Fatalf("Unable to fetch IPv6 management address: %v", err) + } + ipv6 := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Info.address) + ipv6Config := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(configuredIPv6Info.address) + originalPrefixLength := gnmi.Get(t, dut, ipv6.PrefixLength().State()) + + invalidPrefixLength := uint8(129) + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, ipv6Config.PrefixLength().Config(), invalidPrefixLength) + }) + gnmi.Await(t, dut, ipv6.PrefixLength().State(), 5*time.Second, originalPrefixLength) + configuredPrefixLength := gnmi.Get(t, dut, ipv6Config.PrefixLength().Config()) + + if configuredPrefixLength == invalidPrefixLength { + t.Errorf("MGMT component (%v) prefix-length match failed! set:%v, config-path-value:%v (want:%v)", bond0Name, invalidPrefixLength, configuredPrefixLength, originalPrefixLength) + } +} + +func TestSetIPv6InvalidAddress(t *testing.T) { + // This test confirms that an invalid IPv6 address cannot be set. + // Paths tested: + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/config/ip + // /interfaces/interface[name=]/subinterfaces/subinterface[index=]/ipv6/addresses/address[ip=
]/state/ip + defer testhelper.NewTearDownOptions(t).WithID("c58360a6-4d7f-442d-a9f7-9f1d72682ee2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + mockConfigPush(t) + + invalidIPPath := "ffff:ffff:ffff:ffff:ffff:f25c:77ff:fe7f:69be" + configuredIPv6PrefixLength := uint8(64) + ipv6 := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(invalidIPPath) + ipv6Config := gnmi.OC().Interface(bond0Name).Subinterface(interfaceIndex).Ipv6().Address(invalidIPPath) + + d := &oc.Root{} + iface := d.GetOrCreateInterface(bond0Name).GetOrCreateSubinterface(interfaceIndex) + newV6 := iface.GetOrCreateIpv6().GetOrCreateAddress(invalidIPPath) + newV6.Ip = &invalidIPPath + newV6.PrefixLength = &configuredIPv6PrefixLength + + // Cannot write invalid IPv6 address. + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, ipv6Config.Config(), newV6) + }) + + // There should be no IP set with the invalid IPv6 address. + testt.ExpectFatal(t, func(t testing.TB) { + observedIP := gnmi.Get(t, dut, ipv6.Ip().State()) + t.Logf("MGMT component (%v) observed IPv6 address: %v.", bond0Name, observedIP) + }) +} diff --git a/tests/module_reset_test.go b/tests/module_reset_test.go new file mode 100644 index 0000000..0345e1a --- /dev/null +++ b/tests/module_reset_test.go @@ -0,0 +1,213 @@ +package module_reset_test + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + + syspb "github.com/openconfig/gnoi/system" + typespb "github.com/openconfig/gnoi/types" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +const ( + waitTimeInterfacesUp = 60 * time.Second + transceiverPrefix = "Ethernet" +) + +// Enum to represent which modules to reset in each test case. +type testModules int + +const ( + oneModule testModules = iota + allModules +) + +func TestResetModules(t *testing.T) { + dut := ondatra.DUT(t, "DUT") + + // Get all ports connected to peer device. + var dutPorts []string + for _, port := range dut.Ports() { + dutPorts = append(dutPorts, port.Name()) + } + + tests := []struct { + name string + uuid string + modules testModules + }{ + { + name: "TestResetOneModule", + uuid: "4593ff89-892b-46f7-a049-959c8681912d", + modules: oneModule, + }, + { + name: "TestResetAllModules", + uuid: "af9877f2-40f3-4abe-873b-e1db155a917d", + modules: allModules, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID(tt.uuid).Teardown(t) + + operStatusInfo, err := testhelper.FetchPortsOperStatus(t, dut, dutPorts...) + if err != nil { + t.Fatalf("Failed to fetch ports oper status: %v", err) + } + upPorts := operStatusInfo.Up + if len(upPorts) == 0 { + t.Log("No up ports found at start of test") + } + + paths := []*typespb.Path{} + + // Name of transceiver to reset, for OneModule test. + var testXcvr string + + for _, component := range gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) { + xcvrName := component.GetName() + // Skip non-transceiver components. + if !strings.HasPrefix(xcvrName, transceiverPrefix) { + continue + } + if !component.GetEmpty() { + // Add transceiver name to paths. + pathElems := []*typespb.PathElem{ + &typespb.PathElem{Name: "components"}, + &typespb.PathElem{Name: "component", Key: map[string]string{"name": xcvrName}}, + } + path := &typespb.Path{ + Origin: "openconfig", + Elem: pathElems, + } + paths = append(paths, path) + if tt.modules == oneModule { + t.Logf("Testing transceiver %v", xcvrName) + testXcvr = xcvrName + break + } + } + } + if len(paths) == 0 { + t.Fatal("No non-empty transceivers found") + } + req := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: "Reset transceiver modules", + Subcomponents: paths, + } + + params := testhelper.NewRebootParams().WithWaitTime(0 * time.Second).WithCheckInterval(0 * time.Second).WithRequest(req) + + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Reboot RPC failed: %v", err) + } + + // If test resets one module, verify that ports on all the other modules are still up. + if tt.modules == oneModule { + for _, intf := range upPorts { + // Verify that interfaces not on testXcvr are up. + xcvrName := gnmi.Get(t, dut, gnmi.OC().Interface(intf).Transceiver().State()) + if xcvrName != testXcvr { + if got := gnmi.Get(t, dut, gnmi.OC().Interface(intf).OperStatus().State()); got != oc.Interface_OperStatus_UP { + t.Errorf("Interface %v oper status is not UP", intf) + } + } + } + } + + if len(upPorts) > 0 { + time.Sleep(waitTimeInterfacesUp) + if err := testhelper.VerifyPortsOperStatus(t, dut, upPorts...); err != nil { + t.Fatalf("Not all ports are up at the end of the test %v: %v", tt.name, err) + } + } + }) + } +} + +func TestResetModuleInvalidTransceiver(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("9b1c5bb4-f79e-4aab-9488-ce7294728abd").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Get all ports connected to peer device. + var dutPorts []string + for _, port := range dut.Ports() { + dutPorts = append(dutPorts, port.Name()) + } + + operStatusInfo, err := testhelper.FetchPortsOperStatus(t, dut, dutPorts...) + if err != nil { + t.Fatalf("Failed to fetch ports oper status: %v", err) + } + upPorts := operStatusInfo.Up + if len(upPorts) == 0 { + t.Log("No up ports found at start of test") + } + + maxXcvrNum := 0 + + for _, component := range gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) { + xcvrName := component.GetName() + // Skip non-transceiver components. + if !strings.HasPrefix(xcvrName, transceiverPrefix) { + continue + } + var phyPortNum int + _, err := fmt.Sscanf(xcvrName, transceiverPrefix+"%d", &phyPortNum) + if err == nil { + if phyPortNum > maxXcvrNum { + maxXcvrNum = phyPortNum + } + } + } + if maxXcvrNum == 0 { + t.Fatalf("No transceivers found") + } + + invalidXcvrName := fmt.Sprintf("Ethernet%v", maxXcvrNum+1) + t.Logf("Testing with invalid transceiver name %v", invalidXcvrName) + + pathElems := []*typespb.PathElem{ + &typespb.PathElem{Name: "components"}, + &typespb.PathElem{Name: "component", Key: map[string]string{"name": invalidXcvrName}}, + } + path := &typespb.Path{ + Origin: "openconfig", + Elem: pathElems, + } + paths := []*typespb.Path{path} + + req := &syspb.RebootRequest{ + Method: syspb.RebootMethod_COLD, + Message: "Reset transceiver", + Subcomponents: paths, + } + + params := testhelper.NewRebootParams().WithWaitTime(0 * time.Second).WithCheckInterval(0 * time.Second).WithRequest(req) + + err = testhelper.Reboot(t, dut, params) + t.Logf("Reboot err: %v", err) + + if err == nil { + t.Errorf("Reboot RPC expected to fail") + } + if len(upPorts) > 0 { + if err := testhelper.VerifyPortsOperStatus(t, dut, upPorts...); err != nil { + t.Fatalf("Not all ports are up at the end of the test: %v", err) + } + } +} diff --git a/tests/ondatra_test.bzl b/tests/ondatra_test.bzl new file mode 100644 index 0000000..f4756f5 --- /dev/null +++ b/tests/ondatra_test.bzl @@ -0,0 +1,102 @@ +"""Generate Ondatra test definitions.""" + +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +def ondatra_test( + name, + srcs, + testbed = "", + run_timeout = "30m", + args = None, + deps = None, + data = None, + tags = None, + visibility = None): + """Compiles and runs an Ondatra test written in Go. + + Args: + name: Name; required + srcs: List of labels; required + testbed: Label of testbed file; required + run_timeout: Timeout on the test run, excluding the wait time for the + testbed to become available, specified as a duration string + (see http://godoc/pkg/time#ParseDuration); default is 30 minutes + args: List of args: optional + tags: List of arbitrary text tags; optional + deps: List of labels; optional + data: List of labels; optional + visibility: List of visibility labels; optional + """ + data = (data or []) + ["//infrastructure/data"] + testbed = testbed or "infrastructure/data/testbeds.textproto" + testbed_arg = "--testbed=%s" % testbed + + args = (args or []) + [ + testbed_arg, + "--run_time=%s" % run_timeout, + "--wait_time=0", + ] + go_test( + name = name, + srcs = srcs, + deps = deps, + data = data, + args = args, + tags = (tags or []) + ["manual", "exclusive", "external"], + rundir = ".", + visibility = visibility, + size = "enormous", # Reservation is highly variable. + local = True, # Tests cannot run on Forge. + ) + +def ondatra_test_suite( + name, + srcs, + testbeds = {}, + per_test_run_timeout = "30m", + args = None, + deps = None, + data = None, + tags = None, + visibility = None): + """Defines a suite of Ondatra tests written in Go. + + For every (testbed-name, testbed-file) entry in the provided testbeds map, + this rule creates an ondatra_test with the name "[name]_[testbed-name]" and + the testbed equal to testbed-file. + + Args: + name: Name; required + srcs: List of labels; required + testbeds: Map of custom testbed name to testbed label; required + per_test_run_timeout: Timeout on each test run in the suite, excluding the + wait time for the testbed to become available, specified as a duration + string (see http://godoc/pkg/time#ParseDuration); default is 30 minutes + args: List of args: optional + deps: List of labels; optional + data: List of labels; optional + tags: List of arbitrary text tags; optional + visibility: List of visibility labels; optional + """ + if len(testbeds) == 0: + testbeds = {"dualnode" : "infrastructure/data/testbeds.textproto"} + + tests = [] + for testbed_name, testbed_src in testbeds.items(): + test_name = "%s_%s" % (name, testbed_name) + tests.append(test_name) + go_test_tags = (tags or []) + [testbed_name] + ondatra_test( + name = test_name, + srcs = srcs, + testbed = testbed_src, + run_timeout = per_test_run_timeout, + args = args, + deps = deps, + data = data, + tags = go_test_tags, + visibility = visibility, + ) + + # Unlike other tags, "manual" on a test_suite means the suite itself is manual. + native.test_suite(name = name, tests = tests, visibility = visibility, tags = ["manual"]) diff --git a/tests/platforms_hardware_component_test.go b/tests/platforms_hardware_component_test.go new file mode 100644 index 0000000..4697111 --- /dev/null +++ b/tests/platforms_hardware_component_test.go @@ -0,0 +1,908 @@ +package platforms_hardware_component_test + +import ( + "regexp" + "reflect" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/testt" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + + syspb "github.com/openconfig/gnoi/system" +) + +const awaitTime = 5 * time.Second + +func verifyRegexMatch(r string, s string) error { + match, err := regexp.MatchString(r, s) + if err != nil { + return err + } + if !match { + return errors.Errorf("regex match failed, got:%v, want(regex):%v", s, r) + } + + return nil +} + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +// Integrated Circuit tests. +func TestGetICInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("758cf6e4-fac4-4d1c-ba6b-3bf399a66b80").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + ics, err := testhelper.ICInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch integrated-circuit info: %v", err) + } + + for _, ic := range ics { + name := ic.GetName() + componentPath := gnmi.OC().Component(name) + + if got, want := gnmi.Get(t, dut, componentPath.Parent().State()), "chassis"; got != want { + t.Errorf("Integrated circuit component (%v) parent match failed! got:%v, want:%v", name, got, want) + } + + if got, want := gnmi.Get(t, dut, componentPath.Type().State()), oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_INTEGRATED_CIRCUIT; got != want { + t.Errorf("Integrated circuit component (%v) type match failed! got:%v, want:%v", name, got, want) + } + } +} + +func TestGetICErrorInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("bb7e3980-2e6d-4a02-bb42-1872b16d96f7").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + ics, err := testhelper.ICInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch integrated-circuit info: %v", err) + } + + for _, ic := range ics { + name := ic.GetName() + info := gnmi.Get(t, dut, gnmi.OC().Component(name).IntegratedCircuit().Memory().State()) + + if info.CorrectedParityErrors == nil { + t.Errorf("%v doesn't have corrected-parity-errors information", name) + } + if info.TotalParityErrors == nil { + t.Errorf("%v doesn't have total-parity-errors information", name) + } + // If the error information is not present, they will be initialized to 0. + correctedErrors := info.GetCorrectedParityErrors() + totalErrors := info.GetTotalParityErrors() + + // Corrected parity errors should be within defined threshold. + if got, want := correctedErrors, ic.GetCorrectedParityErrorsThreshold(); want != 0 && got > want { + t.Errorf("%v corrected-parity-errors threshold exceeded! got:%v, want:<=%v", name, got, want) + } + // Corrected parity errors cannot be more than the total parity errors. + if correctedErrors > totalErrors { + t.Errorf("%v has more corrected-parity-errors:%v than total-parity-errors:%v", name, correctedErrors, totalErrors) + } + // IC shouldn't have uncorrected errors. + if totalErrors > correctedErrors { + t.Errorf("%v has uncorrected-parity-errors:%v, want:0", name, totalErrors-correctedErrors) + } + } +} + +func TestSetValidICName(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("a255135d-66be-43e8-be05-46e746c033c2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + ics, err := testhelper.ICInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch integrated-circuit info: %v", err) + } + + for _, ic := range ics { + name := ic.GetName() + componentPath := gnmi.OC().Component(name) + + gnmi.Replace(t, dut, componentPath.Name().Config(), name) + gnmi.Await(t, dut, componentPath.Name().State(), awaitTime, name) + + fullyQualifiedName := "abc.def.test.com" + testhelper.ReplaceFullyQualifiedName(t, dut, name, fullyQualifiedName) + testhelper.AwaitFullyQualifiedName(t, dut, name, awaitTime, fullyQualifiedName) + } +} + +func TestSetInvalidICName(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("6632a6dc-eec1-4d8f-9ccb-63b380dc841d").Teardown(t) + + invalidNames := []string{ + "integrated_circuit1234", + "integrated_circuitX", + "invalid_name", + } + + dut := ondatra.DUT(t, "DUT") + ics, err := testhelper.ICInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch integrated-circuit info: %v", err) + } + + for _, ic := range ics { + name := ic.GetName() + configPath := gnmi.OC().Component(name).Name() + statePath := gnmi.OC().Component(name).Name() + + // Set config path so that the corresponding Get() works later on in the test. + gnmi.Replace(t, dut, configPath.Config(), name) + gnmi.Await(t, dut, statePath.State(), awaitTime, name) + + // Configure invalid name values on the DUT. + for _, invalid := range invalidNames { + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, configPath.Config(), invalid) + }) + // Verify that config path doesn't reflect the invalid name. + if got, want := gnmi.Get(t, dut, configPath.Config()), name; got != want { + t.Errorf("Invalid (config) name on %v! got:%v, want:%v", name, got, want) + } + // Verify that state path doesn't reflect the invalid name. + if got, want := gnmi.Get(t, dut, statePath.State()), name; got != want { + t.Errorf("Invalid (state) name on %v! got:%v, want:%v", name, got, want) + } + } + } +} + +func TestSetNodeID(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("7165b086-5d8f-4283-9e4c-aa4a44fe6fbd").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + ics, err := testhelper.ICInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch integrated-circuit info: %v", err) + } + + for _, ic := range ics { + name := ic.GetName() + + nodeID := uint64(12345678) + gnmi.Replace(t, dut, gnmi.OC().Component(name).IntegratedCircuit().NodeId().Config(), nodeID) + gnmi.Await(t, dut, gnmi.OC().Component(name).IntegratedCircuit().NodeId().State(), awaitTime, nodeID) + } +} + +func TestPersistenceAfterReboot(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("0e429a29-d3b1-486b-b3d2-3ca48a9f0c35").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + ics, err := testhelper.ICInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch integrated-circuit info: %v", err) + } + + fullyQualifiedName := "abc.def.test.com" + nodeID := uint64(12345678) + + t.Log("Configuring config paths before reboot") + for _, ic := range ics { + name := ic.GetName() + componentPath := gnmi.OC().Component(name) + + // Configure config paths and verify corresponding state paths. + gnmi.Replace(t, dut, componentPath.Name().Config(), name) + testhelper.ReplaceFullyQualifiedName(t, dut, name, fullyQualifiedName) + testhelper.ReplaceComponentIntegratedCircuitNodeID(t, dut, name, nodeID) + gnmi.Replace(t, dut, componentPath.IntegratedCircuit().NodeId().Config(), nodeID) + time.Sleep(awaitTime) + + info := gnmi.Get(t, dut, componentPath.State()) + if got, want := info.GetName(), name; got != want { + t.Errorf("name verification failed for %v! got:%v, want:%v", name, got, want) + } + if got, want := testhelper.GetFullyQualifiedName(t, dut, name), fullyQualifiedName; got != want { + t.Errorf("fully-qualified-name verification failed for %v! got:%v, want:%v", name, got, want) + } + if got, want := gnmi.Get(t, dut, componentPath.IntegratedCircuit().NodeId().State()), nodeID; got != want { + t.Errorf("node-id verification failed for %v! got:%v, want:%v", name, got, want) + } + } + + // Reboot DUT and verify that the state paths reflect pre-reboot values. + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30 * time.Second).WithRequest(syspb.RebootMethod_COLD) + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Failed to reboot DUT: %v", err) + } + + t.Log("Verifying config and state paths after reboot") + for _, ic := range ics { + name := ic.GetName() + componentPath := gnmi.OC().Component(name) + + stateInfo := gnmi.Get(t, dut, componentPath.State()) + if got, want := stateInfo.GetName(), name; got != want { + t.Errorf("name state path verification failed for %v! got:%v, want:%v", name, got, want) + } + if got, want := testhelper.GetFullyQualifiedName(t, dut, name), fullyQualifiedName; got != want { + t.Errorf("fully-qualified-name state path verification failed for %v! got:%v, want:%v", name, got, want) + } + if got, want := gnmi.Get(t, dut, componentPath.IntegratedCircuit().NodeId().State()), nodeID; got != want { + t.Errorf("node-id state path verification failed for %v! got:%v, want:%v", name, got, want) + } + + configInfo := gnmi.Get(t, dut, componentPath.Config()) + if got, want := configInfo.GetName(), name; got != want { + t.Errorf("name config path verification failed for %v! got:%v, want:%v", name, got, want) + } + if got, want := testhelper.GetFullyQualifiedNameFromConfig(t, dut, name), fullyQualifiedName; got != want { + t.Errorf("fully-qualified-name config path verification failed for %v! got:%v, want:%v", name, got, want) + } + if got, want := gnmi.Get(t, dut, componentPath.IntegratedCircuit().NodeId().Config()), nodeID; got != want { + t.Errorf("node-id config path verification failed for %v! got:%v, want:%v", name, got, want) + } + } +} + +// FPGA tests. +func TestGetFPGAInfo(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("52a71049-40dc-4f2d-b074-4b0f649064f0").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + fpgas, err := testhelper.FPGAInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch FPGA info: %v", err) + } + + var fpgaResetCounts []uint8 + for _, fpga := range fpgas { + name := fpga.GetName() + componentPath := gnmi.OC().Component(name) + wantType := "FPGA" + if gotType := testhelper.FPGAType(t, dut, &fpga); gotType != wantType { + t.Errorf("%v type match failed! got:%v, want:%v", name, gotType, wantType) + } + + if mfgName := gnmi.Get(t, dut, componentPath.MfgName().State()); mfgName != fpga.GetMfgName() { + t.Errorf("%v manufacturer name match failed! got:%v, want:%v", name, mfgName, fpga.GetMfgName()) + } + + if description := gnmi.Get(t, dut, componentPath.Description().State()); description != fpga.GetDescription() { + t.Errorf("%v description match failed! got:%v, want:%v", name, description, fpga.GetDescription()) + } + + if err := verifyRegexMatch(fpga.GetFirmwareVersionRegex(), gnmi.Get(t, dut, componentPath.FirmwareVersion().State())); err != nil { + t.Errorf("%v firmware version match failed! %v", name, err) + } + + resetCauseMap := testhelper.FPGAResetCauseMap(t, dut, &fpga) //fpgaInfo.ResetCause + if got, want := len(resetCauseMap), fpga.GetResetCauseNum(); got != want { + t.Errorf("%v invalid number of reset causes! got:%v, want:%v", name, got, want) + } + for index, resetCause := range resetCauseMap { + if got, want := resetCause.GetIndex(), index; got != want { + t.Errorf("%v reset-cause-index: %v index match failed! got:%v, want:%v", name, index, got, want) + } + if got := resetCause.GetCause(); got < testhelper.ResetCause_Cause_POWER || got > testhelper.ResetCause_Cause_CPU { + t.Errorf("%v reset-cause-index: %v cause match failed! got:%v, want:range(%v-%v)", name, index, got, testhelper.ResetCause_Cause_POWER, testhelper.ResetCause_Cause_CPU) + } + } + + // Need to know current reset count, since after reboot it should be current count + 1. + fpgaResetCounts = append(fpgaResetCounts, testhelper.FPGAResetCount(t, dut, &fpga)) + } + + // Reboot DUT and verify that the latest reset cause is SOFTWARE. + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30 * time.Second).WithRequest(syspb.RebootMethod_COLD) + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Failed to reboot DUT: %v", err) + } + // Wait for the switch to update FPGA information. + time.Sleep(time.Minute) + + for i, fpga := range fpgas { + name := fpga.GetName() + + if got, want := testhelper.FPGAResetCount(t, dut, &fpga), fpgaResetCounts[i]+1; got != want { + t.Errorf("%v latest reset count match failed after reboot! got:%v, want:%v", name, got, want) + } + + if fpga.GetResetCauseNum() == 0 { + // This FPGA doesn't support reset causes. + continue + } + if got, want := testhelper.FPGAResetCause(t, dut, &fpga, 0), testhelper.ResetCause_Cause_SOFTWARE; got != want { + t.Errorf("%v latest reset cause match failed after reboot! got:%v, want:%v", name, got, want) + } + } +} + +// Temperature sensor tests. +func TestGetTemperatureSensorDefaultInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b68ca974-590c-4685-9da4-4c344c74a056").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + type sensorInfo struct { + ocType oc.E_PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT + subType string + } + sensorInfoMap := map[testhelper.TemperatureSensorType]sensorInfo{ + testhelper.CPUTempSensor: { + ocType: oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_CPU, + }, + testhelper.HeatsinkTempSensor: { + ocType: oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_SENSOR, + subType: "HEAT_SINK_TEMPERATURE_SENSOR", + }, + testhelper.ExhaustTempSensor: { + ocType: oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_SENSOR, + subType: "EXHAUST_TEMPERATURE_SENSOR", + }, + testhelper.InletTempSensor: { + ocType: oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_SENSOR, + subType: "INLET_TEMPERATURE_SENSOR", + }, + testhelper.DimmTempSensor: { + ocType: oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_SENSOR, + subType: "DIMM_TEMPERATURE_SENSOR", + }, + } + + tests := []struct { + name string + sensorType testhelper.TemperatureSensorType + }{ + { + name: "CPUTemperatureSensorInfo", + sensorType: testhelper.CPUTempSensor, + }, + { + name: "HeatsinkTemperatureSensorInfo", + sensorType: testhelper.HeatsinkTempSensor, + }, + { + name: "ExhaustTemperatureSensorInfo", + sensorType: testhelper.ExhaustTempSensor, + }, + { + name: "InletTemperatureSensorInfo", + sensorType: testhelper.InletTempSensor, + }, + { + name: "DimmTemperatureSensorInfo", + sensorType: testhelper.DimmTempSensor, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expectedInfo, ok := sensorInfoMap[tt.sensorType] + if !ok { + t.Fatalf("Sensor type: %v not found in expected info map", tt.sensorType) + } + + sensors, err := testhelper.TemperatureSensorInfoForDevice(t, dut, tt.sensorType) + if err != nil { + t.Fatalf("Failed to fetch temperature info for %v: %v", expectedInfo, err) + } + + for _, sensor := range sensors { + name := sensor.GetName() + info := gnmi.Get(t, dut, gnmi.OC().Component(name).State()) + + if got, want := info.GetName(), name; got != want { + t.Errorf("%v name match failed! got:%v, want:%v", name, got, want) + } + if got, want := info.GetParent(), "chassis"; got != want { + t.Errorf("%v parent match failed! got:%v, want:%v", name, got, want) + } + if got, want := info.GetType(), expectedInfo.ocType; got != want { + t.Errorf("%v type match failed! got:%v, want:%v", name, got, want) + } + if got, want := info.GetLocation(), sensor.GetLocation(); got != want { + t.Errorf("%v location match failed! got:%v, want:%v", name, got, want) + } + + // Sensor sub-type is not applicable for CPU temperature sensor. + if tt.sensorType == testhelper.CPUTempSensor { + continue + } + + if got, want := testhelper.SensorType(t, dut, &sensor), expectedInfo.subType; got != want { + t.Errorf("%v sensor sub-type match failed! got:%v, want:%v", name, got, want) + continue + } + } + + }) + } +} + +func TestGetTemperatureSensorTemperatureInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("294bf647-cff4-47d6-a701-ad9dfe7ff8f3").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + tests := []struct { + name string + sensorType testhelper.TemperatureSensorType + }{ + { + name: "CPUTemperatureSensorInfo", + sensorType: testhelper.CPUTempSensor, + }, + { + name: "HeatsinkTemperatureSensorInfo", + sensorType: testhelper.HeatsinkTempSensor, + }, + { + name: "ExhaustTemperatureSensorInfo", + sensorType: testhelper.ExhaustTempSensor, + }, + { + name: "InletTemperatureSensorInfo", + sensorType: testhelper.InletTempSensor, + }, + { + name: "DimmTemperatureSensorInfo", + sensorType: testhelper.DimmTempSensor, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sensors, err := testhelper.TemperatureSensorInfoForDevice(t, dut, tt.sensorType) + if err != nil { + t.Fatalf("Failed to fetch temperature info for sensor type %v: %v", tt.sensorType, err) + } + + for _, sensor := range sensors { + name := sensor.GetName() + if got, want := gnmi.Get(t, dut, gnmi.OC().Component(name).Temperature().Instant().State()), sensor.GetMaxTemperature(); want != 0 && got > want { + t.Errorf("%v temperature threshold exceeded! got:%v, want:<=%v", name, got, want) + } + } + + }) + } +} + +// Health-indicator test. +func TestSetPortHealthIndicator(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("77865f9c-5919-467f-8be2-19a08d6803f9").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + port, err := testhelper.RandomInterface(t, dut, nil) + if err != nil { + t.Fatalf("Failed to fetch random interface: %v", err) + } + + values := []testhelper.E_Interface_HealthIndicator{ + testhelper.Interface_HealthIndicator_BAD, + testhelper.Interface_HealthIndicator_GOOD, + } + for _, healthIndicator := range values { + testhelper.ReplaceHealthIndicator(t, dut, port, healthIndicator) + testhelper.AwaitHealthIndicator(t, dut, port, 5*time.Second, healthIndicator) + } +} + +// Storage device test. +func TestStorageDeviceInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b5db258b-2e3f-4880-96dc-db2ac452afe9").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + devices, err := testhelper.StorageDeviceInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch storage devices: %v", err) + } + + // Removable storage devices may not be present in the switch. This will cause + // dut.Telemetry().Component(name).Get() API to fail fatally. Instead, fetch the + // entire component subtree and validate storage device information. + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) + + for _, device := range devices { + name := device.GetName() + + var info *oc.Component + for _, component := range components { + if component.GetName() == name { + info = component + break + } + } + if info == nil { + if device.GetIsRemovable() == false { + t.Errorf("%v information is missing in DUT", name) + } else { + t.Logf("Skipping verification for removable storage device %v since it is not present in DUT", name) + } + continue + } + t.Logf("Validating information for storage device: %v", name) + + if info.Name == nil { + t.Errorf("%v missing name leaf", name) + } else { + if got, want := info.GetName(), name; got != want { + t.Errorf("%v name match failed! got:%v, want:%v", name, got, want) + } + } + if info.Type == nil { + t.Errorf("%v missing type leaf", name) + } else { + if got, want := info.GetType(), oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_STORAGE; got != want { + t.Errorf("%v type match failed! got:%v, want:%v", name, got, want) + } + } + if info.PartNo == nil { + t.Errorf("%v missing part-no leaf", name) + } else if info.GetPartNo() == "" { + t.Errorf("%v has empty part-no", name) + } + if info.SerialNo == nil { + t.Errorf("%v missing serial-no leaf", name) + } else if info.GetSerialNo() == "" { + t.Errorf("%v has empty serial-no", name) + } + + if info.Removable == nil { + t.Errorf("%v missing removable leaf", name) + } else { + if got, want := info.GetRemovable(), device.GetIsRemovable(); got != want { + t.Errorf("%v removable match failed! got:%v, want:%v", name, got, want) + } + } + + // Only check io-error information for non-removable storage devices. + if device.GetIsRemovable() { + continue + } + if got, want := testhelper.StorageIOErrors(t, dut, &device), device.GetIoErrorsThreshold(); got > want { + t.Errorf("%v io-errors threshold exceeded! got:%v, want:<=%v", name, got, want) + } + } +} + +// Storage device SMART info test. +func TestStorageDeviceSmartInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c5fe2192-9759-4829-9231-8fdb4ecc4245").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + devices, err := testhelper.StorageDeviceInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch storage devices: %v", err) + } + + // Removable storage devices may not be present in the switch. This will cause + // dut.Telemetry().Component(name).Get() API to fail fatally. Instead, fetch the + // entire component subtree and validate storage device information. + components := gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().State()) + + for _, device := range devices { + // Only check SMART information for non-removable storage devices. + if device.GetIsRemovable() { + continue + } + + name := device.GetName() + + var info *oc.Component + for _, component := range components { + if component.GetName() == name { + info = component + break + } + } + if info == nil { + t.Errorf("%v information is missing in DUT", name) + continue + } + t.Logf("Validating SMART information for storage device: %v", name) + + smartDataInfo := device.GetSmartDataInfo() + { + got := testhelper.StorageWriteAmplificationFactor(t, dut, &device) + thresholds := smartDataInfo.GetWriteAmplificationFactorThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v write-amplification-factor thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StorageRawReadErrorRate(t, dut, &device) + thresholds := smartDataInfo.GetRawReadErrorRateThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v raw-read-error-rate thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StorageThroughputPerformance(t, dut, &device) + thresholds := smartDataInfo.GetThroughputPerformanceThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v throughput-performance thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StorageReallocatedSectorCount(t, dut, &device) + thresholds := smartDataInfo.GetReallocatedSectorCountThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v reallocated-sector-count thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StoragePowerOnSeconds(t, dut, &device) + thresholds := smartDataInfo.GetPowerOnSecondsThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v power-on-seconds thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StorageSsdLifeLeft(t, dut, &device) + thresholds := smartDataInfo.GetSsdLifeLeftThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v ssd-life-left thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StorageAvgEraseCount(t, dut, &device) + thresholds := smartDataInfo.GetAvgEraseCountThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v avg-erase-count thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + { + got := testhelper.StorageMaxEraseCount(t, dut, &device) + thresholds := smartDataInfo.GetMaxEraseCountThresholds() + if !thresholds.IsValid(got) { + t.Errorf("%v max-erase-count thresholds not met! got:%v, thresholds:[%v]", + name, got, thresholds) + } + } + } +} + + +// Fan tests. +func TestFanInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("a394f0d4-61a9-45a8-a05a-c738fa4fa4b2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + fans, err := testhelper.FanInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch fan information: %v", err) + } + + for _, fan := range fans { + name := fan.GetName() + // Even though fan components might be removable, we expect all fans to be + // present in the switch (unlike storage devices). Hence, we are fetching + // fan component information instead of fetching the entire component subtree. + info := gnmi.Get(t, dut, gnmi.OC().Component(name).State()) + + if info.Type == nil { + t.Errorf("%v missing type leaf", name) + } else { + if got, want := info.GetType(), oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_FAN; got != want { + t.Errorf("%v type match failed! got:%v, want:%v", name, got, want) + } + } + if info.Location == nil { + t.Errorf("%v missing location leaf", name) + } else { + if got, want := info.GetLocation(), fan.GetLocation(); got != want { + t.Errorf("%v location match failed! got:%v, want:%v", name, got, want) + } + } + if info.Parent == nil { + t.Errorf("%v missing parent leaf", name) + } else { + if got, want := info.GetParent(), fan.GetParent(); got != want { + t.Errorf("%v parent match failed! got:%v, want:%v", name, got, want) + } + } + if info.Removable == nil { + t.Errorf("%v missing removable leaf", name) + } + if got, want := info.GetRemovable(), fan.GetIsRemovable(); got != want { + t.Errorf("%v removable match failed! got:%v, want:%v", name, got, want) + } + if info.Empty == nil { + t.Errorf("%v missing Empty leaf", name) + } else { + if info.GetEmpty() { + t.Errorf("%v is unexpectedly empty.", name) + } + } + + // Only removable fans have FRU information. + if fan.GetIsRemovable() == false { + t.Logf("Not checking FRU information for %v since it is not removable", name) + continue + } + + if info.PartNo == nil { + t.Errorf("%v missing part-no leaf", name) + } else if info.GetPartNo() == "" { + t.Errorf("%v has empty part-no", name) + } + if info.SerialNo == nil { + t.Errorf("%v missing serial-no leaf", name) + } else if info.GetSerialNo() == "" { + t.Errorf("%v has empty serial-no", name) + } + + // Fetch mfg-date leaf separately since we want the test to fail in case + // of non-compliance errors with respect to the date format. Ondatra ignores + // non-compliance errors at sub-tree level Get() but fails the test if there + // is non-compliance at leaf level Get(). + if got := gnmi.Get(t, dut, gnmi.OC().Component(name).MfgDate().State()); got == "" { + t.Errorf("%v has empty mfg-date", name) + } + } + + fantrays, err := testhelper.FanTrayInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch fan information: %v", err) + } + + for _, fantray := range fantrays { + name := fantray.GetName() + // Likewise for fan trays, we expect all to be present regardless of whether they are removable. + info := gnmi.Get(t, dut, gnmi.OC().Component(name).State()) + if info.Type == nil { + t.Errorf("%v missing type leaf", name) + } + // } else { + // if got, want := info.GetType(), oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_FANTRAY; got != want { + // t.Errorf("%v type match failed! got:%v, want:%v", name, got, want) + // } + // } + if info.Location == nil { + t.Errorf("%v missing location leaf", name) + } else { + if got, want := info.GetLocation(), fantray.GetLocation(); got != want { + t.Errorf("%v location match failed! got:%v, want:%v", name, got, want) + } + } + if info.Parent == nil { + t.Errorf("%v missing parent leaf", name) + } else { + if got, want := info.GetParent(), fantray.GetParent(); got != want { + t.Errorf("%v parent match failed! got:%v, want:%v", name, got, want) + } + } + if info.Removable == nil { + t.Errorf("%v missing removable leaf", name) + } + if got, want := info.GetRemovable(), fantray.GetIsRemovable(); got != want { + t.Errorf("%v removable match failed! got:%v, want:%v", name, got, want) + } + if info.Empty == nil { + t.Errorf("%v missing Empty leaf", name) + } else { + if info.GetEmpty() { + t.Errorf("%v is unexpectedly empty.", name) + } + } + } +} + +func TestFanSpeedInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("804f6dbb-5480-4e1d-a215-e259530fa801").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + fans, err := testhelper.FanInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch fan information: %v", err) + } + + for _, fan := range fans { + name := fan.GetName() + info := gnmi.Get(t, dut, gnmi.OC().Component(name).Fan().State()) + + if info.Speed == nil { + t.Errorf("%v missing speed leaf", name) + } else { + if got, want := info.GetSpeed(), fan.GetMaxSpeed(); got > want { + t.Errorf("%v speed threshold exceeded! got:%v, want:<=%v", name, got, want) + } + } + { + if got := testhelper.FanSpeedControlPct(t, dut, &fan); got == 0 || got > 100 { + t.Errorf("%v speed-control-pct failed! got:%v, want:range(0,100]", name, got) + } + } + } +} + +func validatePcieInformation(info any) error { + if info == nil { + return errors.New("PCIe information is nil") + } + + var err error + var totalErrors uint64 + var individualErrors uint64 + rv := reflect.ValueOf(info) + rv = rv.Elem() + for i := 0; i < rv.NumField(); i++ { + name := rv.Type().Field(i).Name + field := rv.Field(i) + if field.IsNil() { + err = testhelper.WrapError(err, "%v leaf is nil", name) + continue + } + field = field.Elem() + if got, want := field.Kind(), reflect.Uint64; got != want { + err = testhelper.WrapError(err, "%v leaf has invalid value type! got:%v, want:%v", name, got, want) + continue + } + + value := field.Uint() + if name == "TotalErrors" { + totalErrors = value + } else { + individualErrors += value + } + } + + if totalErrors > individualErrors { + err = testhelper.WrapError(err, "total-errors:%v should be <= cumulative-individual-errors:%v", totalErrors, individualErrors) + } else if totalErrors == 0 && individualErrors > 0 { + err = testhelper.WrapError(err, "total-errors count cannot be 0 if individual errors are detected (count:%v)", individualErrors) + } + + return err +} + +func TestPcieInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("82e1ef7b-46db-4523-b0e5-f94a2e0a8a12").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + devices, err := testhelper.PcieInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch PCIe information: %v", err) + } + + for _, device := range devices { + name := device.GetName() + + c := gnmi.Get(t, dut, gnmi.OC().Component(name).Pcie().CorrectableErrors().State()) + if err := validatePcieInformation(c); err != nil { + t.Errorf("Correctable error information validation failed for device:%v\n%v", name, err) + } + + f := gnmi.Get(t, dut, gnmi.OC().Component(name).Pcie().FatalErrors().State()) + if err := validatePcieInformation(f); err != nil { + t.Errorf("Fatal error information validation failed for device:%v\n%v", name, err) + } + if f != nil && f.GetTotalErrors() != 0 { + t.Errorf("%v fatal errors detected on %v", f.GetTotalErrors(), dut.Name()) + } + + n := gnmi.Get(t, dut, gnmi.OC().Component(name).Pcie().NonFatalErrors().State()) + if err := validatePcieInformation(n); err != nil { + t.Errorf("Non-fatal error information validation failed for device:%v\n%v", name, err) + } + } +} diff --git a/tests/platforms_software_component_test.go b/tests/platforms_software_component_test.go new file mode 100644 index 0000000..cd6f4f9 --- /dev/null +++ b/tests/platforms_software_component_test.go @@ -0,0 +1,381 @@ +package platforms_software_component_test + +import ( + "net" + "regexp" + "strconv" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/testt" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + + syspb "github.com/openconfig/gnoi/system" +) + +func verifyImageVersion(version string) error { + regex := testhelper.ImageVersionRegex() + + for _, r := range regex { + if match, err := regexp.MatchString(r, version); err == nil && match { + return nil + } + } + + return errors.Errorf("version match failed for %v", version) +} + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func TestGetOperatingSystemDefaultInfo(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("ba4294cc-1174-44c4-88c1-2d3cf8f000a0").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + validStorageSides := map[string]bool{ + "SIDE_A": true, + "SIDE_B": true, + } + + // Software version format: ..-planetbde + swVersionRegex := `^(\d+\.)(\d+\.)(\d+)(-planetbde)$` + expr, err := regexp.Compile(swVersionRegex) + if err != nil { + t.Errorf("Internal error: Invalid regex %v for OS component (%v)", swVersionRegex, err) + } + + // GPINs switch consists of 2 OS paritions - side A and B. The active partition is referenced + // using OS component Openconfig path with key as "os0" and the inactive partition is + // referenced using the OS component Openconfig path with key as "os1". + tests := []struct { + key string + expectedOperStatus oc.E_PlatformTypes_COMPONENT_OPER_STATUS + }{ + { + key: "os0", + expectedOperStatus: oc.PlatformTypes_COMPONENT_OPER_STATUS_ACTIVE, + }, + { + key: "os1", + expectedOperStatus: oc.PlatformTypes_COMPONENT_OPER_STATUS_INACTIVE, + }, + } + + for _, tc := range tests { + componentPath := gnmi.OC().Component(tc.key) + + name := gnmi.Get(t, dut, componentPath.Name().State()) + if name != tc.key { + t.Errorf("OS component (%v) name match failed! got:%v, want:%v", tc.key, name, tc.key) + } + + operStatus := gnmi.Get(t, dut, componentPath.OperStatus().State()) + if operStatus != tc.expectedOperStatus { + t.Errorf("OS component (%v) oper-status match failed! got:%v, want:%v", tc.key, operStatus, tc.expectedOperStatus) + } + + parent := gnmi.Get(t, dut, componentPath.Parent().State()) + if expectedParent := "chassis"; parent != expectedParent { + t.Errorf("OS component (%v) parent match failed! got:%v, want:%v", tc.key, parent, expectedParent) + } + + swVersion := gnmi.Get(t, dut, componentPath.SoftwareVersion().State()) + if expr != nil { + if match := expr.MatchString(swVersion); !match { + t.Errorf("OS component (%v) software version match failed! got:%v, want(Regex):%v", tc.key, swVersion, swVersionRegex) + } + } + + osType := gnmi.Get(t, dut, componentPath.Type().State()) + if expectedOsType := oc.PlatformTypes_OPENCONFIG_SOFTWARE_COMPONENT_OPERATING_SYSTEM; osType != expectedOsType { + t.Errorf("OS component (%v) type match failed! got:%v, want:%v", tc.key, osType, expectedOsType) + } + + storageSide := testhelper.ComponentStorageSide(t, dut, name) + if _, ok := validStorageSides[storageSide]; ok { + // Storage side needs to be unique for each OS component. Remove the current + // side from valid storage sides map. + delete(validStorageSides, storageSide) + } else { + t.Errorf("Invalid storage-side for OS component (%v)! got:%v", tc.key, storageSide) + } + } +} + +func TestGetBootloaderDefaultInfo(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("d66c9503-4458-45aa-b816-fb75ed01e46d").Teardown(t) + dut := ondatra.DUT(t, "DUT") + key := "boot_loader" + componentPath := gnmi.OC().Component(key) + + name := gnmi.Get(t, dut, componentPath.Name().State()) + if name != key { + t.Errorf("Bootloader component name match failed! got:%v, want:%v", name, key) + } + + parent := gnmi.Get(t, dut, componentPath.Parent().State()) + if expectedParent := "chassis"; parent != expectedParent { + t.Errorf("Bootloader component parent match failed! got:%v, want:%v", parent, expectedParent) + } + + swVersion := gnmi.Get(t, dut, componentPath.SoftwareVersion().State()) + // Version format: ..* + swVersionRegex := `^(\d+\.)(\d+\.).*$` + if match, err := regexp.MatchString(swVersionRegex, swVersion); err != nil || !match { + t.Errorf("Bootloader component software version match failed! got:%v, want(Regex):%v %v", swVersion, swVersionRegex, err) + } + + compType := gnmi.Get(t, dut, componentPath.Type().State()) + if expectedCompType := oc.PlatformTypes_OPENCONFIG_SOFTWARE_COMPONENT_BOOT_LOADER; compType != expectedCompType { + t.Errorf("Bootloader component type match failed! got:%v, want:%v", compType, expectedCompType) + } +} + +func TestGetNetworkStackDefaultInfo(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("092c1229-c8a4-4941-bbd8-a7fe1ae79a48").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + validStorageSides := map[string]bool{ + "SIDE_A": true, + "SIDE_B": true, + } + + // GPINs switch consists of 2 paritions - side A and B. The active partition is referenced + // using network stack component Openconfig path with key as "network_stack0" and the + // inactive partition is referenced using network stack component Openconfig path with key as + // "network_stack1". + tests := []struct { + key string + expectedOperStatus oc.E_PlatformTypes_COMPONENT_OPER_STATUS + }{ + { + key: "network_stack0", + expectedOperStatus: oc.PlatformTypes_COMPONENT_OPER_STATUS_ACTIVE, + }, + { + key: "network_stack1", + expectedOperStatus: oc.PlatformTypes_COMPONENT_OPER_STATUS_INACTIVE, + }, + } + + for _, tc := range tests { + componentPath := gnmi.OC().Component(tc.key) + + name := gnmi.Get(t, dut, componentPath.Name().State()) + if name != tc.key { + t.Errorf("Network stack component (%v) name match failed! got:%v, want:%v", tc.key, name, tc.key) + } + + operStatus := gnmi.Get(t, dut, componentPath.OperStatus().State()) + if operStatus != tc.expectedOperStatus { + t.Errorf("Network stack component (%v) oper-status match failed! got:%v, want:%v", tc.key, operStatus, tc.expectedOperStatus) + } + + parent := gnmi.Get(t, dut, componentPath.Parent().State()) + if expectedParent := "chassis"; parent != expectedParent { + t.Errorf("Network stack component (%v) parent match failed! got:%v, want:%v", tc.key, parent, expectedParent) + } + + swVersion := gnmi.Get(t, dut, componentPath.SoftwareVersion().State()) + // Image version check is applicable to only for active image in the current run. Refer b/221157028. + if operStatus == oc.PlatformTypes_COMPONENT_OPER_STATUS_ACTIVE { + if err := verifyImageVersion(swVersion); err != nil { + t.Errorf("Network stack component (%v) software version match failed! got:%v, want(Regex):%v", tc.key, swVersion, testhelper.ImageVersionRegex()) + } + } + + networkStackType := gnmi.Get(t, dut, componentPath.Type().State()) + if expectedNetworkStackType := oc.PlatformTypes_OPENCONFIG_SOFTWARE_COMPONENT_SOFTWARE_MODULE; networkStackType != expectedNetworkStackType { + t.Errorf("Network stack component (%v) type match failed! got:%v, want:%v", tc.key, networkStackType, expectedNetworkStackType) + } + + moduleType := gnmi.Get(t, dut, componentPath.SoftwareModule().ModuleType().State()) + if expectedModuleType := oc.PlatformSoftware_SOFTWARE_MODULE_TYPE_USERSPACE_PACKAGE_BUNDLE; moduleType != expectedModuleType { + t.Errorf("Network stack component (%v) module-type match failed! got:%v, want:%v", tc.key, moduleType, expectedModuleType) + } + + storageSide := testhelper.ComponentStorageSide(t, dut, name) + if _, ok := validStorageSides[storageSide]; ok { + // Storage side needs to be unique for each network stack component. Remove the current + // side from valid storage sides map. + delete(validStorageSides, storageSide) + } else { + t.Errorf("Invalid storage-side for network stack component (%v)! got:%v", tc.key, storageSide) + } + } +} + +func TestGetChassisDefaultMacAddress(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("0bbc650c-d1a7-42d4-b2a3-e19a4336366a").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + name := "chassis" + baseMacAddress := testhelper.ComponentChassisBaseMacAddress(t, dut, name) + if _, err := net.ParseMAC(baseMacAddress); err != nil { + t.Errorf("Invalid base-mac-address format received for chassis! got:%v", baseMacAddress) + } + + poolSize := testhelper.ComponentChassisMacAddressPoolSize(t, dut, name) + if !(poolSize >= 1) { + t.Errorf("Chassis component pool size match failed! got:%v, want:(value >= 1)", poolSize) + } +} + +func TestGetChassisDefaultInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("1fdac1f2-c790-4871-9e44-89c91e0b0161").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + key := "chassis" + componentPath := gnmi.OC().Component(key) + + firmwareVersion := gnmi.Get(t, dut, componentPath.FirmwareVersion().State()) + if err := verifyImageVersion(firmwareVersion); err != nil { + t.Errorf("Chassis component firmware version match failed! got:%v, want(Regex):%v", firmwareVersion, testhelper.ImageVersionRegex()) + } + + fqdn := testhelper.GetFullyQualifiedName(t, dut, key) + fqdnRegex := testhelper.SwitchNameRegex() + if match, err := regexp.MatchString(fqdnRegex, fqdn); err != nil || !match { + t.Errorf("Chassis component fully-qualified-name match failed! got:%v, want(Regex):%v %v", fqdn, fqdnRegex, err) + } + + hardwareVersionStr := gnmi.Get(t, dut, componentPath.HardwareVersion().State()) + hardwareVersion, err := strconv.Atoi(hardwareVersionStr) + if err != nil { + t.Fatalf("Failed to convert chassis hardware version %v to int", hardwareVersionStr) + } + if !((hardwareVersion >= 0) && (hardwareVersion <= 255)) { + t.Errorf("Chassis component hardware version match failed! got:%v, want:[0, 255]", hardwareVersion) + } + + // mfg-date format is verified by the Get() API. If the format is incorrect, the + // Get() API would fail the test. Therefore, no additional validation is required. + gnmi.Get(t, dut, componentPath.MfgDate().State()) + + name := gnmi.Get(t, dut, componentPath.Name().State()) + if name != key { + t.Errorf("Chassis component name match failed! got:%v, want:%v", name, key) + } + + partNo := gnmi.Get(t, dut, componentPath.PartNo().State()) + maxLength := 20 + if len(partNo) > maxLength { + t.Errorf("Chassis component part-no length validation failed! got:%v, want:%v(atmost)", len(partNo), maxLength) + } + + if platform := testhelper.ComponentChassisPlatform(t, dut, name); platform != "experimental" { + t.Errorf("Chassis component platform match failed! got:%v, want:experimental", platform) + } + + serialNo := gnmi.Get(t, dut, componentPath.SerialNo().State()) + if len(serialNo) > maxLength { + t.Errorf("Chassis component serial-no length validation failed! got:%v, want:%v(atmost)", len(serialNo), maxLength) + } + + compType := gnmi.Get(t, dut, componentPath.Type().State()) + if expectedCompType := oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_CHASSIS; compType != expectedCompType { + t.Errorf("Chassis component type match failed! got:%v, want%v", compType, expectedCompType) + } + + if modelName := testhelper.ComponentChassisModelName(t, dut, name); modelName == "" { + t.Errorf("Chassis component model-name match failed! got:%v, want:non-empty string", modelName) + } +} + +func TestSetChassisNamePaths(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("6c7dc60b-1b11-459c-8f80-0c8c7bcc0375").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + key := "chassis" + componentPath := gnmi.OC().Component(key) + + testStrings := []string{ + "abc.s1.test.com", + "def.s2.test.com", + "xyz.xyz16.test.com", + } + + for _, fqdn := range testStrings { + testhelper.ReplaceFullyQualifiedName(t, dut, key, fqdn) + testhelper.AwaitFullyQualifiedName(t, dut, key, 5*time.Second, fqdn) + if got, want := testhelper.GetFullyQualifiedNameFromConfig(t, dut, key), fqdn; got != want { + t.Errorf("Chassis component (config) fully-qualified-name match failed! got:%v, want:%v", got, want) + } + } + + // Verify that the switch is able to configure the same chassis name. + name := gnmi.Get(t, dut, componentPath.Name().State()) + gnmi.Replace(t, dut, componentPath.Name().Config(), name) + gnmi.Await(t, dut, componentPath.Name().State(), 5*time.Second, name) + if got, want := gnmi.Get(t, dut, componentPath.Name().Config()), name; got != want { + t.Errorf("Chassis component (config) name match failed! got:%v, want:%v", got, want) + } +} + +func TestSetChassisInvalidName(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("af16797f-e1e1-4aa6-9414-56d2a0b52052").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + key := "chassis" + configPath := gnmi.OC().Component(key).Name() + statePath := gnmi.OC().Component(key).Name() + + // Set config path so that the corresponding Get() works later on in the test. + gnmi.Replace(t, dut, configPath.Config(), key) + gnmi.Await(t, dut, statePath.State(), 5*time.Second, key) + + invalidNames := []string{ + "xyz", + "mychassis", + "invalidchassisname", + } + + for _, name := range invalidNames { + testt.ExpectFatal(t, func(t testing.TB) { + gnmi.Replace(t, dut, configPath.Config(), name) + }) + // Verify that config path doesn't reflect the invalid name. + if got, want := gnmi.Get(t, dut, configPath.Config()), key; got != want { + t.Errorf("Chassis component (config) name match failed! got:%v, want:%v", got, want) + } + // Verify that state path doesn't reflect the invalid name. + if got, want := gnmi.Get(t, dut, statePath.State()), key; got != want { + t.Errorf("Chassis component (state) name match failed! got:%v, want:%v", got, want) + } + } +} + +func TestChassisInfoPersistsAfterReboot(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("8c3c4325-4459-473b-90b6-a919fbd0ddfe").Teardown(t) + dut := ondatra.DUT(t, "DUT") + key := "chassis" + + // Configure fully-qualified-name on the chassis. + fqdn := "xy120ab012.xyz.test.com" + testhelper.ReplaceFullyQualifiedName(t, dut, key, fqdn) + testhelper.AwaitFullyQualifiedName(t, dut, key, 5*time.Second, fqdn) + + // Reboot switch and verify that the previous fully-qualified-name persists + // after reboot. + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30 * time.Second).WithRequest(syspb.RebootMethod_COLD) + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Failed to reboot DUT: %v", err) + } + + if got, want := testhelper.GetFullyQualifiedName(t, dut, key), fqdn; got != want { + t.Errorf("fully-qualified-name state match failed! got:%v, want:%v", got, want) + } + if got, want := testhelper.GetFullyQualifiedNameFromConfig(t, dut, key), fqdn; got != want { + t.Errorf("fully-qualified-name config match failed! got:%v, want:%v", got, want) + } +} diff --git a/tests/port_debug_data_test.go b/tests/port_debug_data_test.go new file mode 100644 index 0000000..2fc368b --- /dev/null +++ b/tests/port_debug_data_test.go @@ -0,0 +1,74 @@ +package port_debug_data_test + +import ( + "fmt" + "testing" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func TestGetPortDebugDataInvalidInterface(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("dba77fa7-b0d1-4412-8136-22dea24ed935").Teardown(t) + var intfName = "Ethernet99999" + err := testhelper.HealthzGetPortDebugData(t, ondatra.DUT(t, "DUT"), intfName); + if err == nil { + t.Fatalf("Expected RPC failure due to invalid interface %v", intfName) + } +} + +func TestGetPortDebugDataWithTranscevierInserted(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("8f0468c5-6b2c-477c-9cb2-ec099a686268").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + frontPanelPorts, err := testhelper.FrontPanelPortListForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch front panel ports with error %v", err) + } + + for _, intfName := range frontPanelPorts { + xcvrName := gnmi.Get(t, dut, gnmi.OC().Interface(intfName).Transceiver().State()) + if gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Empty().State()) { + // Skip the interfaces without transceiver inserted. + continue + } + + t.Logf("Get port debug data from interface %v on xcvr present port %v", intfName, xcvrName) + err := testhelper.HealthzGetPortDebugData(t, dut, intfName) + if err != nil { + t.Fatalf("Expected RPC success, got error %v", err) + } + + } +} + +func TestGetPortDebugDataWithoutTranscevierInserted(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("2229d2e5-1e0b-415b-ac23-b5b05f76e6d4").Teardown(t) + dut := ondatra.DUT(t, "DUT") + frontPanelPorts, err := testhelper.FrontPanelPortListForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch front panel ports") + } + + for _, intfName := range frontPanelPorts { + xcvrName := gnmi.Get(t, dut, gnmi.OC().Interface(intfName).Transceiver().State()) + if !gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Empty().State()) { + // Skip the interfaces with transceiver inserted. + fmt.Println(intfName + " : " + xcvrName) + continue + } + + t.Logf("Get port debug data from interface %v on xcvr empty port %v", intfName, xcvrName) + err := testhelper.HealthzGetPortDebugData(t, dut, intfName) + if err != nil { + t.Fatalf("Expected RPC success, got error %v", err) + } + + } +} diff --git a/tests/system_paths_test.go b/tests/system_paths_test.go new file mode 100644 index 0000000..7a9338a --- /dev/null +++ b/tests/system_paths_test.go @@ -0,0 +1,804 @@ +package system_paths_test + +import ( + "fmt" + "math/rand" + "strings" + "testing" + "time" + + log "github.com/golang/glog" + "github.com/google/go-cmp/cmp" + "github.com/openconfig/ondatra" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "github.com/pkg/errors" + + syspb "github.com/openconfig/gnoi/system" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func verifyAddress(address string, addresses []string) error { + for _, addr := range addresses { + if addr == address { + return nil + } + } + return errors.New("unknown address") +} + +func TestGetRemoteServerAddressInfo(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c2873412-1016-4c89-9e59-79fcfec642bb").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + logInfo, err := testhelper.LoggingServerAddressesForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch remote server logging info: %v", err) + } + + // Collect remote server addresses. + foundAddresses := gnmi.GetAll(t, dut, gnmi.OC().System().Logging().RemoteServerAny().Host().State()) + + // Determine if configured addresses are IPv4 or IPv6. We are only allowed to have one or the other. + hasIpv4, hasIpv6 := false, false + for _, addr := range foundAddresses { + if err := verifyAddress(addr, logInfo.IPv4RemoteAddresses); err == nil { + hasIpv4 = true + } + if err := verifyAddress(addr, logInfo.IPv6RemoteAddresses); err == nil { + hasIpv6 = true + } + } + + if !hasIpv4 && !hasIpv6 { + t.Fatalf("Remote server addresses do not match device logging server addresses: got: %v vs want: %v or want: %v ", strings.Join(foundAddresses, ", "), strings.Join(logInfo.IPv4RemoteAddresses, ", "), strings.Join(logInfo.IPv6RemoteAddresses, ", ")) + } + if hasIpv4 && hasIpv6 { + t.Fatalf("Remote server addresses are not expected to mix IPv4 and IPv6 addresses: got: %v", strings.Join(foundAddresses, ", ")) + } + + addresses := logInfo.IPv4RemoteAddresses + if hasIpv6 { + addresses = logInfo.IPv6RemoteAddresses + } + + // Addresses configured may only be what device configuration allows. + if foundLen, addressLen := len(foundAddresses), len(addresses); foundLen != addressLen { + t.Errorf("Unexpected number of remote logging server addresses: %v (want %v).", foundLen, addressLen) + } + + addressSet := make(map[string]bool) + for _, addr := range foundAddresses { + addressSet[addr] = true + } + // Addresses may not be repeated. + if setLen, foundLen := len(addressSet), len(foundAddresses); setLen != foundLen { + t.Errorf("Remote logging addresses are not unique: %v", foundAddresses) + } + + // Addresses configured may only be what device configuration allows. + for _, addr := range foundAddresses { + if err := verifyAddress(addr, addresses); err != nil { + t.Errorf("Remote logging address is unsupported: %v", addr) + } + } + + // Check that state host value matches the rest of the path. + for _, addr := range foundAddresses { + if readAddress := gnmi.Get(t, dut, gnmi.OC().System().Logging().RemoteServer(addr).Host().State()); readAddress != addr { + t.Errorf("Remote logging host address does not match path: %v vs %v", readAddress, addr) + } + } +} + +func TestGetCurrentDateAndTime(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("8ec03425-b9ab-4e13-8b01-1564b5043d68").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + t1 := time.Now() + time.Sleep(1 * time.Second) + dutTime, err := time.Parse(time.RFC3339, gnmi.Get(t, dut, gnmi.OC().System().CurrentDatetime().State())) + if err != nil { + t.Fatalf("Failed to parse DUT time: %v", err) + } + t2 := time.Now() + + // Time reported by DUT should be between the time the request was sent and received. + if dutTime.Before(t1) || dutTime.After(t2) { + t.Errorf("Time comparison failed! got:%v, want:(greater than:%v, less than:%v)", dutTime, t1, t2) + } +} + +func TestGetBootTime(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c2bcb460-e79a-4ae2-9a74-d1b3d6ec62ae").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // boot-time should be the same before rebooting switch. We give a 1 second buffer to account for + // jitter in boot-time calculation. + want := gnmi.Get(t, dut, gnmi.OC().System().BootTime().State()) + time.Sleep(5 * time.Second) + sec := uint64(time.Second.Nanoseconds()) + if got := gnmi.Get(t, dut, gnmi.OC().System().BootTime().State()); got < want-sec || got > want+sec { + t.Errorf("boot-time comparison before reboot failed! got:%v, want:%v(+-1s)", got, want) + } + + waitTime, err := testhelper.RebootTimeForDevice(t, dut) + if err != nil { + t.Fatalf("Unable to get reboot wait time: %v", err) + } + params := testhelper.NewRebootParams().WithWaitTime(waitTime).WithCheckInterval(30 * time.Second).WithRequest(syspb.RebootMethod_COLD) + if err := testhelper.Reboot(t, dut, params); err != nil { + t.Fatalf("Failed to reboot DUT: %v", err) + } + + // boot-time should be later than the previous boot-time after rebooting switch. + if got := gnmi.Get(t, dut, gnmi.OC().System().BootTime().State()); got <= want { + t.Errorf("boot-time comparison after reboot failed! got:%v, want:(greater than)%v", got, want) + } +} + +func TestGetHostname(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("01c119ae-2550-4949-8fd7-3605b8d2981c").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + hostname := gnmi.Get(t, dut, gnmi.OC().System().Hostname().State()) + if len(hostname) == 0 || len(hostname) > 253 { + t.Errorf("Invalid hostname length! got:%v, want:(0-253)", len(hostname)) + } + if hostname != dut.Name() { + t.Errorf("Hostname match failed! got:%v, want:%v", hostname, dut.Name()) + } +} + +func TestConfigMetaData(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("366f4520-79f7-49ac-a67d-c53b48b11535").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Perform an initial config push on config-meta-data path. + // TODO: Remove this step when default config push is available. + testhelper.ReplaceConfigMetaData(t, dut, "initial metadata") + + origMetaData := testhelper.SystemConfigMetaData(t, dut) + if len(origMetaData) == 0 { + t.Error("Invalid initial metadata length! got:0, want:(greater than) 0") + } + // Configure a different value of at config-meta-data path. + newMetaData := "test1" + if newMetaData == origMetaData { + newMetaData = "test2" + } + + testhelper.ReplaceConfigMetaData(t, dut, newMetaData) + + if got, want := testhelper.SystemConfigMetaData(t, dut), newMetaData; got != want { + t.Errorf("Invalid value for config-meta-data state path! got:%v, want:%v", got, want) + } + if got, want := testhelper.SystemConfigMetaDataFromConfig(t, dut), newMetaData; got != want { + t.Errorf("Invalid value for config-meta-data config path! got:%v, want:%v", got, want) + } +} + +func TestCPUIndexes(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("093c4411-c748-4b7c-bee7-fd73b8c2a473").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + cpuInfo, err := testhelper.CPUInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch CPU information: %v", err) + } + + // Convert index in expected CPU information to System_Cpu_Index_Union type since this + // type will be returned in the GET response by the switch. + wantIndexes := make(map[oc.System_Cpu_Index_Union]bool) + for _, cpu := range cpuInfo { + index, err := (&oc.System_Cpu{}).To_System_Cpu_Index_Union(cpu.GetIndex()) + if err != nil { + t.Fatalf("To_System_Cpu_Index_Union() failed for index:%v (%v)", cpu.GetIndex(), err) + } + wantIndexes[index] = true + } + + gotIndexes := make(map[oc.System_Cpu_Index_Union]bool) + for i, info := range gnmi.GetAll(t, dut, gnmi.OC().System().CpuAny().State()) { + if info.Index == nil { + t.Errorf("CPU index not present in information iteration %v", i) + continue + } + gotIndexes[info.GetIndex()] = true + } + + if !cmp.Equal(wantIndexes, gotIndexes) { + t.Errorf("CPU index match failed! (-want +got):%v", cmp.Diff(wantIndexes, gotIndexes)) + } +} + +func TestCPUUsage(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("4806f97b-1c4e-4763-a9e3-58671bda144a").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + cpuInfo, err := testhelper.CPUInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch CPU information: %v", err) + } + + wantUsage := make(map[oc.System_Cpu_Index_Union]uint8) + for _, cpu := range cpuInfo { + index, err := (&oc.System_Cpu{}).To_System_Cpu_Index_Union(cpu.GetIndex()) + if err != nil { + t.Fatalf("To_System_Cpu_Index_Union() failed for index:%v (%v)", cpu.GetIndex(), err) + } + wantUsage[index] = cpu.GetMaxAverageUsage() + } + + gotInfo := gnmi.GetAll(t, dut, gnmi.OC().System().CpuAny().State()) + if len(gotInfo) != len(wantUsage) { + t.Errorf("Invalid number of CPU indexes received from switch! got:%v, want:%v", len(gotInfo), len(wantUsage)) + } + + // Fetch the average utilization for each CPU for 2 minutes. In each iteration, validate + // that the utilization is less than the specified threshold. Also, store the cumulative + // utilization for each CPU. At the end of 2 minutes, validate that the cumulative + // utilization for each CPU is non-zero since it is highly unlikely that a CPU is not + // being utilized during this entire time interval. + waitTime := 2 * time.Minute + interval := 10 * time.Second + cumulativeUsage := make(map[oc.System_Cpu_Index_Union]int) + log.Infof("Fetching average CPU utilization in %v intervals for %v", interval, waitTime) + for timeout := time.Now().Add(waitTime); time.Now().Before(timeout); { + log.Info("========== CPU average usage stats ==========") + for i, info := range gotInfo { + if info.Index == nil { + t.Errorf("CPU index not present in information iteration %v", i) + continue + } + + index := info.GetIndex() + if _, ok := wantUsage[index]; !ok { + t.Errorf("Invalid index:%v received from DUT", index) + continue + } + + got := gnmi.Get(t, dut, gnmi.OC().System().Cpu(index).Total().Avg().State()) + if wantUsage[index] != 0 && got > wantUsage[index] { + t.Errorf("CPU (index:%v) average usage validation failed! got:%v, want:<%v", index, got, wantUsage[index]) + } + log.Infof("CPU (index:%v): %v", index, got) + cumulativeUsage[index] += int(got) + } + + time.Sleep(interval) + } + + for i, u := range cumulativeUsage { + if u == 0 { + t.Errorf("CPU (index:%v) cumulative average got:0, want:>0", i) + } + } + +} + +func TestCPUInterval(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("534f595e-06b6-434c-b7cb-20d856efacdb").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + cpuInfo, err := testhelper.CPUInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch CPU information: %v", err) + } + + wantIndexes := make(map[oc.System_Cpu_Index_Union]bool) + for _, cpu := range cpuInfo { + index, err := (&oc.System_Cpu{}).To_System_Cpu_Index_Union(cpu.GetIndex()) + if err != nil { + t.Fatalf("To_System_Cpu_Index_Union() failed for index:%v (%v)", cpu.GetIndex(), err) + } + wantIndexes[index] = true + } + + gotInfo := gnmi.GetAll(t, dut, gnmi.OC().System().CpuAny().State()) + for i, info := range gotInfo { + if info.Index == nil { + t.Errorf("CPU index not present in information iteration %v", i) + continue + } + + index := info.GetIndex() + if _, ok := wantIndexes[index]; !ok { + t.Errorf("Invalid index:%v received from DUT", index) + continue + } + if got := gnmi.Get(t, dut, gnmi.OC().System().Cpu(index).Total().Interval().State()); got == 0 { + t.Errorf("CPU (index:%v) interval validation failed! got:%v, want:>0", index, got) + } + } + + if len(gotInfo) != len(wantIndexes) { + t.Errorf("Invalid number of CPU indexes received from switch! got:%v, want:%v", len(gotInfo), len(wantIndexes)) + } +} + +// This method performs validations on process information leafs. +// It returns whether the validations need to be retried along with the error +// encountered while performing the validations. +// The condition for retry is that there is a missing leaf for the PID. If the +// leaf validation itself fails, the API returns retry = false along with the +// errors encountered. +func validateProcessInformation(procInfo *oc.System_Process, bootTime uint64, systemMemory uint64) (bool, error) { + var err error + infoMissing := false + validationFailed := false + pid := procInfo.GetPid() + + processString := func() string { + name := procInfo.GetName() + if name == "" { + name = "" + } + return fmt.Sprintf("%s (pid:%d)", name, pid) + } + + if procInfo.CpuUtilization == nil { + infoMissing = true + err = testhelper.WrapError(err, "Invalid cpu-utilization for %v! got:, want:range(0-100)", processString()) + } else if got := procInfo.GetCpuUtilization(); got > 100 { + // Not checking for UMF default value since actual CPU utilization + // of the process can be 0. + validationFailed = true + err = testhelper.WrapError(err, "Invalid cpu-utilization for %v! got:%v, want:range(0-100)", processString()) + } + + if procInfo.MemoryUsage == nil { + infoMissing = true + err = testhelper.WrapError(err, "Invalid memory-usage for %v! got:, want:(<=)%v", processString(), systemMemory) + } else if got := procInfo.GetMemoryUsage(); got > systemMemory { + // Not checking for UMF default value since actual memory usage + // of the process can be 0. + validationFailed = true + err = testhelper.WrapError(err, "Invalid memory-usage for %v! got:%v, want:(<=)%v", processString(), got, systemMemory) + } + + if procInfo.StartTime == nil { + infoMissing = true + err = testhelper.WrapError(err, "Invalid start-time for %v! got:, want:(>=)%v", processString(), bootTime) + } else { + got := procInfo.GetStartTime() + if got == 0 { + // UMF sends 0 by default. start-time of a process cannot be 0. + // This indicates missing DB information. + infoMissing = true + err = testhelper.WrapError(err, "Invalid start-time for %v! got:%v, want:(>=)%v", processString(), got, bootTime) + } else if got < bootTime { + validationFailed = true + err = testhelper.WrapError(err, "Invalid start-time for %v! got:%v, want:(>=)%v", processString(), got, bootTime) + } + } + + if procInfo.Name == nil { + infoMissing = true + err = testhelper.WrapError(err, "Invalid name for pid:%v! got:, want:", pid) + } else if procInfo.GetName() == "" { + // UMF sends empty name by default. name of a process cannot be empty. + // This indicates missing DB information. + infoMissing = true + err = testhelper.WrapError(err, "Invalid name for pid:%v! got:, want:", pid) + } + + // CPU usage time might not be present since the process might be ephemeral. In such cases, + // only log that the information is not present. + if procInfo.CpuUsageUser == nil { + log.Infof("cpu-usage-user not reported for %v", processString()) + } + if procInfo.CpuUsageSystem == nil { + log.Infof("cpu-usage-system not reported for %v", processString()) + } + + if validationFailed { + // Don't retry if validation failed for a particular leaf. + return false, err + } + + // None of the validations failed, so retry if information for a leaf + // is missing. + return infoMissing, err +} + +func TestMemoryStatistics(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("d2f4917b-3813-4e81-b195-8f6b9222d615").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + expectedInfo, err := testhelper.MemoryInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch memory information for device: %v", err) + } + info := gnmi.Get(t, dut, gnmi.OC().System().Memory().State()) + + if info.Physical == nil { + t.Error("Physical memory information not received from DUT") + } else { + // Physical memory value returned by the switch might not be an exact match. + // Provide 1GB error margin. + errMargin := uint64(1073741824) + if got, want := info.GetPhysical(), expectedInfo.GetPhysical(); got > want || want-got > errMargin { + t.Errorf("Physical memory validation failed! got:%v, want:%v (error margin: -%v)", got, want, errMargin) + } + } + + if info.Free == nil { + t.Error("Free memory information not received from DUT") + } else { + if got, want := info.GetFree(), info.GetPhysical(); got > want { + t.Errorf("Free memory (%v) more than physical memory (%v)", got, want) + } + if expectedInfo.GetFreeThreshold() != 0 { + // Free memory threshold specified for the device. + if got, want := info.GetFree(), expectedInfo.GetFreeThreshold(); got < want { + t.Errorf("Free memory threshold validation failed! got:%v, want:>=%v", got, want) + } + } + } + + if info.Used == nil { + t.Error("Used memory information not received from DUT") + } else { + if got, want := info.GetUsed(), info.GetPhysical(); got > want { + t.Errorf("Used memory (%v) more than physical memory (%v)", got, want) + } + if expectedInfo.GetUsedThreshold() != 0 { + // Used memory threshold specified for the device. + if got, want := info.GetUsed(), expectedInfo.GetUsedThreshold(); got > want { + t.Errorf("Used memory threshold validation failed! got:%v, want:<=%v", got, want) + } + } + } +} + +func TestMemoryErrorStatistics(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("6300ee99-ac15-4913-a8a8-9231bb92a498").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + expectedInfo, err := testhelper.MemoryInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch memory information for device: %v", err) + } + info := gnmi.Get(t, dut, gnmi.OC().System().Memory().Counters().State()) + + if info.CorrectableEccErrors == nil { + t.Errorf("correctable-ecc-errors information not received from DUT") + } else { + if got, want := info.GetCorrectableEccErrors(), expectedInfo.GetCorrectableEccErrorThreshold(); want != 0 && got > want { + t.Errorf("correctable-ecc-errors threshold exceeded! got:%v, want:<=%v", got, want) + } + } + + if info.UncorrectableEccErrors == nil { + t.Errorf("uncorrectable-ecc-errors information not received from DUT") + } else { + if got := info.GetUncorrectableEccErrors(); got != 0 { + t.Errorf("uncorrectable-ecc-errors detected on the DUT! got:%v, want:0", got) + } + } +} + +func TestNTPServerInformation(t *testing.T) { + dut := ondatra.DUT(t, "DUT") + + tests := []struct { + name string + uuid string + checkAllInformation bool + }{ + { + name: "TestServerAddress", + uuid: "a0cd293a-0a26-4b2a-bf8f-3819e885ed1a", + }, + { + name: "TestServerInfo", + uuid: "0b83e93f-5d85-4100-ba81-a5f1af058763", + checkAllInformation: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + expectedInfo, err := testhelper.NTPServerInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch NTP server information for device: %v", err) + } + + ntp := gnmi.Get(t, dut, gnmi.OC().System().Ntp().State()) + if ntp == nil { + t.Fatalf("No NTP information received from DUT") + } + serverInfo := ntp.Server + + // Each NTP server IP address is returned separately from the DUT. + // Therefore, all expected IP addresses need to be aggregated before + // comparing with the received information. + expectedServerNum := 0 + for _, info := range expectedInfo { + expectedServerNum += len(info.GetIPv4Address()) + len(info.GetIPv6Address()) + } + + if got, want := len(serverInfo), expectedServerNum; got != want { + t.Errorf("Invalid number of NTP servers! got:%v, want:%v", got, want) + } + + for _, info := range expectedInfo { + // For each expected NTP server, fetch the IPv4 and IPv6 address from the + // gNMI response and perform validations. + ipv4Reachable := false + ipv6Reachable := false + expectedAddresses := []struct { + addresses []string + isIPv4 bool + }{ + { + addresses: info.GetIPv4Address(), + isIPv4: true, + }, + { + addresses: info.GetIPv6Address(), + }, + } + for _, e := range expectedAddresses { + for _, address := range e.addresses { + // Ensure that the server address is present in the gNMI response. + log.Infof("Validating NTP server: %s", address) + if _, ok := serverInfo[address]; !ok { + t.Errorf("%v NTP server not reported by DUT", address) + continue + } + server := serverInfo[address] + if got, want := server.GetAddress(), address; got != want { + t.Errorf("%v NTP server has invalid address field! got:%v, want:%v", address, got, want) + } + + // Only perform additional checks if checkAllInformation is set. + if !tt.checkAllInformation { + continue + } + + // Do not perform value checks for unreachable servers. + // Only ensure that fields are present in the response. + checkValue := false + + if server.Stratum == nil { + t.Errorf("%v NTP server doesn't have stratum information", address) + } else if checkValue { + if got, want := server.GetStratum(), info.GetStratumThreshold(); want != 0 && got > want { + t.Errorf("%v NTP server has invalid stratum field! got:%v, want:<=%v", address, got, want) + } + } + + if server.RootDelay == nil { + t.Errorf("%v NTP server doesn't have root-delay information", address) + } else if checkValue { + if got := server.GetRootDelay(); got == 0 { + t.Errorf("%v NTP server has invalid root-delay field! got:%v, want:>0", address, got) + } + } + + if server.PollInterval == nil { + t.Errorf("%v NTP server doesn't have poll-interval information", address) + } else { + // Poll interval value should always be checked since the NTP + // client must poll the server at non-zero intervals. + if got := server.GetPollInterval(); got == 0 { + t.Errorf("%v NTP server has invalid poll-interval field! got:%v, want:>0", address, got) + } + } + + if server.RootDispersion == nil { + t.Errorf("%v NTP server doesn't have root-dispersion information", address) + } else if checkValue { + if got := server.GetRootDispersion(); got == 0 { + t.Errorf("%v NTP server has invalid root-dispersion field! got:%v, want:>0", address, got) + } + } + } + } + + // Server should be reachable via an IPv4 or IPv6 address. + if tt.checkAllInformation && !ipv4Reachable && !ipv6Reachable { + ipAddresses := append(info.GetIPv4Address(), info.GetIPv6Address()...) + t.Errorf("NTP server with IP addresses: %v is not reachable", strings.Join(ipAddresses, ",")) + } + } + }) + } +} + +func TestMountPointsInformation(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("c0cf81e1-e99d-4b57-b273-9fe87c713881").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + + mountPoints, err := testhelper.MountPointsInfoForDevice(t, dut) + if err != nil { + t.Fatalf("Failed to fetch mount points information for device: %v", err) + } + + // Validate that DUT returns at least the required number of mount points. + if got := gnmi.GetAll(t, dut, gnmi.OC().System().MountPointAny().State()); len(got) < len(mountPoints) { + t.Errorf("Invalid number of mount points! got:%v, want:>=%v", len(got), len(mountPoints)) + } + + for _, mp := range mountPoints { + name := mp.GetName() + info := gnmi.Get(t, dut, gnmi.OC().System().MountPoint(name).State()) + + if info.Size == nil { + t.Errorf("%v missing size leaf", name) + } else if info.GetSize() == 0 { + t.Errorf("%v has invalid size! got:0, want:>0", name) + } + + if info.Available == nil { + t.Errorf("%v missing available leaf", name) + } + + if info.Size != nil && info.Available != nil { + if size, available := info.GetSize(), info.GetAvailable(); available > size { + t.Errorf("available space:%v exceeds size:%v for mount point %v", available, size, name) + } + } + } +} + +func printProcessStatistics(t *testing.T, stats map[uint64]oc.System_Process) { + logString := "\n***************************************\n" + logString += "\tProcess Statistics" + logString += "\n***************************************\n" + for pid, info := range stats { + logString += fmt.Sprintf("Process: %s\n", info.GetName()) + logString += fmt.Sprintf("PID: %d\n", pid) + startTime := info.GetStartTime() + // Nanoseconds to seconds. + divider := uint64(1000000000) + logString += fmt.Sprintf("Start Time: %d (%s)\n", startTime, time.Unix(int64(startTime/divider), int64(startTime%divider))) + m := info.GetMemoryUsage() / 1000000 + suffix := "MB" + if m > 1000 { + m = m / 1000 + suffix = "GB" + } + logString += fmt.Sprintf("Memory Usage: %d bytes (%d %s)\n", info.GetMemoryUsage(), m, suffix) + logString += fmt.Sprintf("Memory Utilization: %d%%\n", info.GetMemoryUtilization()) + logString += fmt.Sprintf("CPU Utilization: %d%%\n", info.GetCpuUtilization()) + logString += "--------------------------------------------------------------------\n" + } + t.Log(logString) +} + +// This is a GPINs-specific test since it makes assumptions about the +// functionality of systemstatsd back-end. +func TestProcessStatistics(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("dc958005-d45b-429d-9ee1-c14cc1eefcf2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + info := gnmi.GetAll(t, dut, gnmi.OC().System().ProcessAny().State()) + if len(info) == 0 { + t.Fatalf("Invalid number of PIDs! got:0, want:>=1") + } + bootTime := gnmi.Get(t, dut, gnmi.OC().System().BootTime().State()) + systemMemory := gnmi.Get(t, dut, gnmi.OC().System().Memory().Physical().State()) + + // Systemstatsd updates process attributes one-by-one in the DB in the + // following order: + // cpu-utilization, memory-usage, start-time, name. + // If any of the above attributes are empty or not present, retry fetching + // information for that process. If that also fails, only then declare the + // test as failure. + var retryPID []uint64 + stats := make(map[uint64]oc.System_Process) + for _, procInfo := range info { + pid := procInfo.GetPid() + if pid == 0 { + t.Errorf("Invalid PID value! got:0, want:>=1") + continue + } + stats[pid] = *procInfo + + retry, err := validateProcessInformation(procInfo, bootTime, systemMemory) + if retry { + log.Infof("Adding pid:%v to retry list. Failures seen:\n %v", pid, err) + retryPID = append(retryPID, pid) + } else if err != nil { + // At least one validation failed. + t.Error(err) + } + } + + time.Sleep(1 * time.Second) + for _, pid := range retryPID { + log.Infof("Retrying information validation for pid:%v", pid) + procInfo := gnmi.Get(t, dut, gnmi.OC().System().Process(pid).State()) + if _, err := validateProcessInformation(procInfo, bootTime, systemMemory); err != nil { + t.Errorf("Validation failed for pid:%v\n %v", pid, err) + } + stats[pid] = *procInfo + } + + printProcessStatistics(t, stats) +} + +func generateLabel(existingLabelsSubtree []*testhelper.System_FeatureLabel) (label uint32, ok bool) { + currLabels := map[uint32]bool{} + for _, val := range existingLabelsSubtree { + currLabels[val.GetLabel()] = true + } + // Loop (for a maximum of 100 times) until a label is generated that is not an existing label on + // the switch. Exit if unable to generate a suitable label. + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 100; i++ { + label = rand.Uint32() + if !currLabels[label] { + return label, true + } + } + return label, false +} + +func TestFeatureLabels(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("b5c1a559-21b6-4fa0-b5ff-1f15080b7b0f").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + // Get the existing feature-labels tree. + existingLabelsSubtree := testhelper.SystemFeatureLabels(t, dut) + + // Generate a unique label not already configured on the switch. + label, labelGenerated := generateLabel(existingLabelsSubtree) + if !labelGenerated { + t.Fatalf("Couldn't generate a new label from feature-labels: %v", existingLabelsSubtree) + } + + featureLabel := testhelper.CreateFeatureLabel(label) + gnmi.Replace(t, dut, testhelper.SystemFeatureLabelPath(gnmi.OC().System(), label).Config(), featureLabel) + testhelper.AwaitSystemFeatureLabel(t, dut, 5*time.Second, featureLabel) + + defer func() { + // Remove the configured feature-label after the test. + gnmi.Delete(t, dut, testhelper.SystemFeatureLabelPath(gnmi.OC().System(), label).Config()) + labelsSubtree := testhelper.SystemFeatureLabels(t, dut) + if len(labelsSubtree) != len(existingLabelsSubtree) { + t.Errorf("Incorrect number of feature-labels found after FeatureLabel(%v).Delete; got:%v, want:%v", label, len(labelsSubtree), len(existingLabelsSubtree)) + } + for _, val := range labelsSubtree { + if val.GetLabel() == label { + t.Errorf("Path did not get deleted; got:%v, want:", label) + } + } + }() + + // Get the new feature-labels tree after Set Replace. + newLabelsSubtree := testhelper.SystemFeatureLabels(t, dut) + if got, want := len(newLabelsSubtree), len(existingLabelsSubtree)+1; got != want { + t.Fatalf("Incorrect number of feature-labels found after FeatureLabel(%v).Replace; got:%v, want:%v", label, got, want) + } + + // Verify that the new feature-label is present in the feature-labels subtree. + labelFound := false + for _, val := range newLabelsSubtree { + if val.GetLabel() == label { + labelFound = true + break + } + } + if !labelFound { + t.Fatalf("Couldn't find configured label: %v in feature-labels: %v", label, newLabelsSubtree) + } + + // Verify the GET response for the feature-label state and config leaf paths. + if got, want := testhelper.SystemFeatureLabel(t, dut, label).GetLabel(), label; got != want { + t.Errorf("gnmi.Get(t, dut, gnmi.OC().System().FeatureLabel(label).State()).GetLabel() got:%v, want:%v", got, want) + } + if got, want := testhelper.SystemFeatureLabelFromConfig(t, dut, label).GetLabel(), label; got != want { + t.Errorf("gnmi.GetConfig(t, dut, gnmi.OC().System().FeatureLabel(label).Config()).GetLabel() got:%v, want:%v", got, want) + } +} diff --git a/tests/transceiver_test.go b/tests/transceiver_test.go new file mode 100644 index 0000000..54bdd83 --- /dev/null +++ b/tests/transceiver_test.go @@ -0,0 +1,325 @@ +package transceiver_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + "github.com/openconfig/ondatra/gnmi/oc" + "github.com/openconfig/ygot/ygot" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/binding/pinsbind" + "github.com/sonic-net/sonic-mgmt/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper" + "google.golang.org/grpc" +) + +func TestMain(m *testing.M) { + ondatra.RunTests(m, pinsbind.New) +} + +func FindPresentTransceiver(t *testing.T, dut *ondatra.DUTDevice) (string, int) { + for _, xcvrName := range gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().Name().State()) { + var phyPortNum int + _, err := fmt.Sscanf(xcvrName, "Ethernet%d", &phyPortNum) + if err == nil { + empty := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Empty().State()) + if !empty { + return xcvrName, phyPortNum + } + } + } + t.Fatal("No non-empty transceiver found") + return "", 0 +} + +func xcvrLanesByXcvrName(t *testing.T, dut *ondatra.DUTDevice, xcvrName string) (uint16, error) { + formFactor := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Transceiver().FormFactor().State()) + xcvrTypeToLanes := map[oc.E_TransportTypes_TRANSCEIVER_FORM_FACTOR_TYPE]uint16{ + oc.TransportTypes_TRANSCEIVER_FORM_FACTOR_TYPE_SFP: 1, + oc.TransportTypes_TRANSCEIVER_FORM_FACTOR_TYPE_SFP_PLUS: 1, + oc.TransportTypes_TRANSCEIVER_FORM_FACTOR_TYPE_QSFP: 4, + oc.TransportTypes_TRANSCEIVER_FORM_FACTOR_TYPE_QSFP_PLUS: 4, + oc.TransportTypes_TRANSCEIVER_FORM_FACTOR_TYPE_OSFP: 8, + } + if lane, ok := xcvrTypeToLanes[formFactor]; ok { + return lane, nil + } + return 0, fmt.Errorf("transceiver %v has unsupported form factor %v", xcvrName, formFactor.String()) +} + +func FindPresentOpticalTransceiver(t *testing.T, dut *ondatra.DUTDevice) (string, int, error) { + gnmiClient, err := dut.RawAPIs().BindingDUT().DialGNMI(context.Background(), grpc.WithBlock()) + if err != nil { + t.Fatalf("Unable to get gNMI client (%v)", err) + } + for _, xcvrName := range gnmi.GetAll(t, dut, gnmi.OC().ComponentAny().Name().State()) { + var phyPortNum int + _, err := fmt.Sscanf(xcvrName, "Ethernet%d", &phyPortNum) + if err == nil { + empty := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Empty().State()) + if !empty { + optical, err := IsOptical(gnmiClient, dut, xcvrName) + if err != nil { + return "", 0, fmt.Errorf("IsOptical failed: %v", err.Error()) + } + if optical { + return xcvrName, phyPortNum, nil + } + } + } + } + return "", 0, nil +} + +// Check if a transceiver is optical by getting cable-length, +// which should only be positive only for copper tranceivers. +// If cable-length is 0, then the transceiver is optical. +// openconfig-platform-ext.yang which is unavailable to Ondatra, +// it is necessary to use raw gNMI get. +func IsOptical(gnmiClient gpb.GNMIClient, dut *ondatra.DUTDevice, xcvrName string) (bool, error) { + prefix := &gpb.Path{Origin: "openconfig", Target: dut.Name()} + sPath, err := ygot.StringToStructuredPath("components/component[name=" + xcvrName + "]/transceiver/state/openconfig-platform-ext:cable-length") + if err != nil { + return false, fmt.Errorf("Unable to convert string to path (%v)", err) + } + paths := []*gpb.Path{sPath} + getRequest := &gpb.GetRequest{ + Prefix: prefix, + Path: paths, + Type: gpb.GetRequest_STATE, + Encoding: gpb.Encoding_PROTO, + } + ctx := context.Background() + getResp, err := gnmiClient.Get(ctx, getRequest) + if err != nil { + return false, fmt.Errorf("Unable to fetch get client (%v)", err) + } + if getResp == nil { + return false, fmt.Errorf("Unable to fetch get client, get response is nil") + } + notifs := getResp.GetNotification() + if len(notifs) != 1 { + return false, fmt.Errorf("got %d notifications, want 1", len(notifs)) + } + notif := notifs[0] + updates := notif.GetUpdate() + if len(updates) != 1 { + return false, fmt.Errorf("got %d updates in the notification, want 1", len(updates)) + } + val := updates[0].GetVal() + return val.GetFloatVal() == 0, nil +} + +func TestReadName(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("bbdc5e8b-8182-4a55-a7bd-11fc206aedc2").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + xcvrName, _ := FindPresentTransceiver(t, dut) + + telemetryNamePath := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Name().State()) + if xcvrName != telemetryNamePath { + t.Errorf("Component key name (%v) does not match telemetry name path value: %v", xcvrName, telemetryNamePath) + } + + configNamePath := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Name().Config()) + if xcvrName != configNamePath { + t.Errorf("Component key name (%v) does not match config name path value: %v", xcvrName, configNamePath) + } +} + +func TestIndex(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("2e63771a-4414-459b-a4ef-d56ed0de6a7a").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + xcvrName, _ := FindPresentTransceiver(t, dut) + t.Logf("Transceiver found: %v", xcvrName) + + numChannels, err := xcvrLanesByXcvrName(t, dut, xcvrName) + if err != nil { + t.Fatalf("%v", err) + } + + for channel := uint16(0); channel < numChannels; channel++ { + if channel != gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Transceiver().Channel(channel).Index().State()) { + t.Errorf("Failed to get telemetry channel index %v", channel) + } + if channel != gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Transceiver().Channel(channel).Index().Config()) { + t.Errorf("Failed to get config channel index %v", channel) + } + } + + // TODO: uncomment after bug is fixed. + // Out-of-range get index should fail. + // testt.ExpectError(t, func(t testing.TB) { + // dutTelemetry.Component(xcvrName).Transceiver().Channel(numChannels).Index().Get(t) + // }) +} + +func TestReadTransceiverStaticData(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("5b255989-eb9d-4587-922f-160b9a011cf1").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + + xcvrName, xcvrNum := FindPresentTransceiver(t, dut) + + if len(gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).MfgName().State())) == 0 { + t.Errorf("Transceiver %v MfgName is empty", xcvrName) + } + if len(gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).PartNo().State())) == 0 { + t.Errorf("Transceiver %v PartNo is empty", xcvrName) + } + if len(gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).SerialNo().State())) == 0 { + t.Errorf("Transceiver %v SerialNo is empty", xcvrName) + } + if len(gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).FirmwareVersion().State())) == 0 { + t.Errorf("Transceiver %v FirmwareVersion is empty", xcvrName) + } + if len(testhelper.GetLatestAvailableFirmwareVersion(t, dut, xcvrName)) == 0 { + t.Errorf("Transceiver %v LatestAvailableFirmwareVersion is empty", xcvrName) + } + + // Get() API for leaf nodes verifies that value complies with the format, + // specified in the YANG model, which will verify that the + // date is in YYYY-MM-DD format. + gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).MfgDate().State()) + + if ethernetPmd := testhelper.EthernetPMD(t, dut, xcvrName); ethernetPmd == "ETH_UNDEFINED" { + t.Errorf("Transceiver %v has undefined PMD type", xcvrName) + } + + componentType := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Type().State()) + + if componentType != oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_TRANSCEIVER { + t.Errorf("Transceiver %v has incorrect component type: %v, should be TRANSCEIVER", xcvrName, componentType) + } + + expectedParent := fmt.Sprintf("1/%v", xcvrNum) + parent := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Parent().State()) + if parent != expectedParent { + t.Errorf("Transceiver (%v) parent match failed! got:%v, want:%v", xcvrName, parent, expectedParent) + } +} + +// TODO: Move platform-specific constants to testhelper. +// These ranges are arbitrary, and not necessarily the operational range for production. +const ( + minTemp = 10.0 + maxTemp = 55.0 + minPower = -30.0 + maxPower = 10.0 + unsupportedPower = -40.0 +) + +// TODO: Once SetTransceiverState is implemented and supported in gNMI, +// and xcvrd threading issues are fixed, also test that input-power changes. +func TestReadTransceiverDynamicData(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("31c2b81d-a3b5-4841-a2ad-da4bcc1f4a8f").Teardown(t) + dut := ondatra.DUT(t, "DUT") + + xcvrName, _, err := FindPresentOpticalTransceiver(t, dut) + + if err != nil { + t.Fatalf("FindPresentOpticalTransceiver failed, %v", err.Error()) + } + + if xcvrName == "" { + t.Log("No optical transceiver found, skipping test") + t.Skip() + } else { + t.Logf("Testing transceiver %v", xcvrName) + } + + temperature := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Temperature().Instant().State()) + if temperature < minTemp || temperature > maxTemp { + t.Errorf("Transceiver temperature is %v, should be in range 10C-55C", temperature) + } + + numChannels, err := xcvrLanesByXcvrName(t, dut, xcvrName) + if err != nil { + t.Fatalf("%v", err) + } + + for channel := uint16(0); channel < numChannels; channel++ { + laserBiasCurrent := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Transceiver().Channel(channel).LaserBiasCurrent().Instant().State()) + if laserBiasCurrent <= 0.0 { + t.Errorf("Laser bias current for channel %v is %v, should be positive", channel, laserBiasCurrent) + } + inputPower := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Transceiver().Channel(channel).InputPower().Instant().State()) + if inputPower < minPower || inputPower > maxPower { + t.Errorf("Input power for channel %v is %v, should be in range [%v, %v]", channel, inputPower, minPower, maxPower) + } + outputPower := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).Transceiver().Channel(channel).OutputPower().Instant().State()) + + // Output power is not supported on some modules: if unsupported, the value is reported as -40. + // Skip testing output power in this case. + if (outputPower != unsupportedPower) && (outputPower < minPower || outputPower > maxPower) { + t.Errorf("Output power for channel %v is %v, should be in range [%v, %v]", channel, outputPower, minPower, maxPower) + } + } + +} + +func TestReadParentPath(t *testing.T) { + defer testhelper.NewTearDownOptions(t).WithID("0057ca44-0cbc-4054-a20c-94bc99e9b984").Teardown(t) + + dut := ondatra.DUT(t, "DUT") + + xcvrName, xcvrNum := FindPresentTransceiver(t, dut) + + transceiver := gnmi.Get(t, dut, gnmi.OC().Component(xcvrName).State()) + + transceiverPathName := transceiver.GetName() + if transceiverPathName != xcvrName { + t.Errorf("Transceiver parent path name got: %v, want: %v", transceiverPathName, xcvrName) + } + + if len(transceiver.GetMfgName()) == 0 { + t.Errorf("Transceiver %v MfgName is empty", xcvrName) + } + + mfgDate := transceiver.GetMfgDate() + dateMatched, err := regexp.MatchString(`^\d\d\d\d-\d\d-\d\d$`, mfgDate) + if err != nil { + t.Errorf("MatchString for MfgDate failed: %v", err) + } else if !dateMatched { + t.Errorf("MfgDate is %v, should be in format YYYY-MM-DD", mfgDate) + } + + if len(transceiver.GetPartNo()) == 0 { + t.Errorf("Transceiver %v SerialNo is empty", xcvrName) + } + if len(transceiver.GetSerialNo()) == 0 { + t.Errorf("Transceiver %v PartNo is empty", xcvrName) + } + if len(transceiver.GetFirmwareVersion()) == 0 { + t.Errorf("Transceiver %v FirmwareVersion is empty", xcvrName) + } + if len(testhelper.GetLatestAvailableFirmwareVersion(t, dut, xcvrName)) == 0 { + t.Errorf("Transceiver %v LatestAvailableFirmwareVersion is empty", xcvrName) + } + + componentType := transceiver.GetType() + if componentType != oc.PlatformTypes_OPENCONFIG_HARDWARE_COMPONENT_TRANSCEIVER { + t.Errorf("Transceiver %v has incorrect component type: %v, should be TRANSCEIVER", xcvrName, componentType) + } + + expectedParent := fmt.Sprintf("1/%v", xcvrNum) + parent := transceiver.GetParent() + if parent != expectedParent { + t.Errorf("Transceiver (%v) parent match failed! got:%v, want:%v", xcvrName, parent, expectedParent) + } + + numChannels, err := xcvrLanesByXcvrName(t, dut, xcvrName) + if err != nil { + t.Fatalf("%v", err) + } + + for channel := uint16(0); channel < numChannels; channel++ { + if channel != transceiver.GetTransceiver().GetChannel(channel).GetIndex() { + t.Errorf("Failed to get channel index %v", channel) + } + } +}