diff --git a/sdn_tests/pins_ondatra/.gitignore b/sdn_tests/pins_ondatra/.gitignore new file mode 100644 index 00000000000..e923bbc11ab --- /dev/null +++ b/sdn_tests/pins_ondatra/.gitignore @@ -0,0 +1,4 @@ +bazel-* +*.pem +*.cnf +*.orig diff --git a/sdn_tests/pins_ondatra/BUILD b/sdn_tests/pins_ondatra/BUILD new file mode 100644 index 00000000000..565db34f194 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/README.md b/sdn_tests/pins_ondatra/README.md new file mode 100644 index 00000000000..b9f3a5b5cca --- /dev/null +++ b/sdn_tests/pins_ondatra/README.md @@ -0,0 +1,29 @@ +# 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/sdn_tests/pins_ondatra/WORKSPACE.bazel b/sdn_tests/pins_ondatra/WORKSPACE.bazel new file mode 100644 index 00000000000..6b582fc6a3e --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/ghodss_yaml.patch b/sdn_tests/pins_ondatra/bazel/patches/ghodss_yaml.patch new file mode 100644 index 00000000000..011d484f3e8 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/gnmi-001-fix_virtual_proto_import.patch b/sdn_tests/pins_ondatra/bazel/patches/gnmi-001-fix_virtual_proto_import.patch new file mode 100644 index 00000000000..ca3deff5d2f --- /dev/null +++ b/sdn_tests/pins_ondatra/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"], diff --git a/sdn_tests/pins_ondatra/bazel/patches/gnmi.patch b/sdn_tests/pins_ondatra/bazel/patches/gnmi.patch new file mode 100644 index 00000000000..a07686d3ab9 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/gnoi.patch b/sdn_tests/pins_ondatra/bazel/patches/gnoi.patch new file mode 100644 index 00000000000..4520fe9fc63 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/gnoigo.patch b/sdn_tests/pins_ondatra/bazel/patches/gnoigo.patch new file mode 100644 index 00000000000..7693d6416fa --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/gnsi.patch b/sdn_tests/pins_ondatra/bazel/patches/gnsi.patch new file mode 100644 index 00000000000..c295d55a993 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/gribi.patch b/sdn_tests/pins_ondatra/bazel/patches/gribi.patch new file mode 100644 index 00000000000..fc6cac07526 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/grpc-001-fix_file_watcher_race_condition.patch b/sdn_tests/pins_ondatra/bazel/patches/grpc-001-fix_file_watcher_race_condition.patch new file mode 100644 index 00000000000..af5d1510f80 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/grpc-003-fix_go_gazelle_register_toolchain.patch b/sdn_tests/pins_ondatra/bazel/patches/grpc-003-fix_go_gazelle_register_toolchain.patch new file mode 100644 index 00000000000..bfed9023aca --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/ondatra.patch b/sdn_tests/pins_ondatra/bazel/patches/ondatra.patch new file mode 100644 index 00000000000..9f3b83b6402 --- /dev/null +++ b/sdn_tests/pins_ondatra/bazel/patches/ondatra.patch @@ -0,0 +1,51 @@ +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/sdn_tests/pins_ondatra/bazel/patches/p4lang.patch b/sdn_tests/pins_ondatra/bazel/patches/p4lang.patch new file mode 100644 index 00000000000..ee9f8dd17f0 --- /dev/null +++ b/sdn_tests/pins_ondatra/bazel/patches/p4lang.patch @@ -0,0 +1,24 @@ +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" + ], + ) diff --git a/sdn_tests/pins_ondatra/bazel/patches/rules_proto_grpc.patch b/sdn_tests/pins_ondatra/bazel/patches/rules_proto_grpc.patch new file mode 100644 index 00000000000..7d78ff80277 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/bazel/patches/snappi.patch b/sdn_tests/pins_ondatra/bazel/patches/snappi.patch new file mode 100644 index 00000000000..fcae40d9004 --- /dev/null +++ b/sdn_tests/pins_ondatra/bazel/patches/snappi.patch @@ -0,0 +1,53 @@ +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/sdn_tests/pins_ondatra/bazel/patches/ygnmi.patch b/sdn_tests/pins_ondatra/bazel/patches/ygnmi.patch new file mode 100644 index 00000000000..38252ca93c5 --- /dev/null +++ b/sdn_tests/pins_ondatra/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", diff --git a/sdn_tests/pins_ondatra/bazel/patches/ygot.patch b/sdn_tests/pins_ondatra/bazel/patches/ygot.patch new file mode 100644 index 00000000000..d90ae26c56c --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infra_deps.bzl b/sdn_tests/pins_ondatra/infra_deps.bzl new file mode 100644 index 00000000000..e5222ec4a63 --- /dev/null +++ b/sdn_tests/pins_ondatra/infra_deps.bzl @@ -0,0 +1,337 @@ +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/sdn_tests/pins_ondatra/infrastructure/binding/BUILD.bazel b/sdn_tests/pins_ondatra/infrastructure/binding/BUILD.bazel new file mode 100644 index 00000000000..0d746a7516f --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/binding/BUILD.bazel @@ -0,0 +1,56 @@ +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 = [ + ":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 = [ + ":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", + ], +) diff --git a/sdn_tests/pins_ondatra/infrastructure/binding/binding_backend.go b/sdn_tests/pins_ondatra/infrastructure/binding/binding_backend.go new file mode 100644 index 00000000000..69bd7b2a951 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/binding/pins_backend.go b/sdn_tests/pins_ondatra/infrastructure/binding/pins_backend.go new file mode 100644 index 00000000000..825190215d3 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/binding/pins_backend.go @@ -0,0 +1,203 @@ +// 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" + + 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" +) + +// 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, + } + } + + 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) { + 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))) + 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/sdn_tests/pins_ondatra/infrastructure/binding/pins_binding.go b/sdn_tests/pins_ondatra/infrastructure/binding/pins_binding.go new file mode 100644 index 00000000000..ed8250e3c3f --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/certs/BUILD.bazel b/sdn_tests/pins_ondatra/infrastructure/certs/BUILD.bazel new file mode 100644 index 00000000000..97f471eecbb --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/certs/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +filegroup( + name = "certs", + srcs = glob(["*.pem"]), +) diff --git a/sdn_tests/pins_ondatra/infrastructure/certs/crt_gen.sh b/sdn_tests/pins_ondatra/infrastructure/certs/crt_gen.sh new file mode 100755 index 00000000000..73133a5e91c --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/data/BUILD.bazel b/sdn_tests/pins_ondatra/infrastructure/data/BUILD.bazel new file mode 100644 index 00000000000..40105d7c303 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/data/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_visibility = ["//visibility:public"], + licenses = ["notice"], +) + +filegroup( + name = "data", + srcs = glob(["**"]), +) diff --git a/sdn_tests/pins_ondatra/infrastructure/data/config.json b/sdn_tests/pins_ondatra/infrastructure/data/config.json new file mode 100644 index 00000000000..491247e3dcf --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/data/config.json @@ -0,0 +1,3 @@ +{ + "openconfig-interfaces:interfaces" : {} +} diff --git a/sdn_tests/pins_ondatra/infrastructure/data/p4rtconfig.prototext b/sdn_tests/pins_ondatra/infrastructure/data/p4rtconfig.prototext new file mode 100644 index 00000000000..0b9f05ffd0d --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/data/p4rtconfig.prototext @@ -0,0 +1,3 @@ +pkg_info{ + name : "sampleP4" version : "1.0" arch : "arch" organization : "org" +} tables {} diff --git a/sdn_tests/pins_ondatra/infrastructure/data/testbeds.textproto b/sdn_tests/pins_ondatra/infrastructure/data/testbeds.textproto new file mode 100644 index 00000000000..9765c114211 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/BUILD.bazel b/sdn_tests/pins_ondatra/infrastructure/testhelper/BUILD.bazel new file mode 100644 index 00000000000..3f15bb4d4ff --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/augment.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/augment.go new file mode 100644 index 00000000000..643bf9c08cb --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/gnmi.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/gnmi.go new file mode 100644 index 00000000000..be185ced8d9 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/testhelper/gnmi.go @@ -0,0 +1,90 @@ +package testhelper + +import ( + "context" + "testing" + + "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 +} diff --git a/sdn_tests/pins_ondatra/infrastructure/testhelper/gnoi.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/gnoi.go new file mode 100644 index 00000000000..de06454f072 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/testhelper/gnoi.go @@ -0,0 +1,97 @@ +package testhelper + +// This file contains helper method for gNOI services such as +// Reboot, Install etc. +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/gnmi" + + 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 != "" +} + +// 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/sdn_tests/pins_ondatra/infrastructure/testhelper/lacp.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/lacp.go new file mode 100644 index 00000000000..18d50eecd24 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/testhelper/lacp.go @@ -0,0 +1,42 @@ +package testhelper + +import ( + "github.com/openconfig/ondatra/gnmi/oc" +) + +// 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 +} + +// 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, + } +} diff --git a/sdn_tests/pins_ondatra/infrastructure/testhelper/p4rt.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/p4rt.go new file mode 100644 index 00000000000..e9f5092008c --- /dev/null +++ b/sdn_tests/pins_ondatra/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("infrastructure/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/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_components.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_components.go new file mode 100644 index 00000000000..fbf4c71aacf --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info.go new file mode 100644 index 00000000000..57345ae37ca --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info/BUILD.bazel b/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info/BUILD.bazel new file mode 100644 index 00000000000..c33a96e7f52 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info/default.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info/default.go new file mode 100644 index 00000000000..8b6c09bdf2c --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info/platform_info.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/platform_info/platform_info.go new file mode 100644 index 00000000000..eb727a6cdab --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/port_management.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/port_management.go new file mode 100644 index 00000000000..dc5d6348c02 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/testhelper/port_management.go @@ -0,0 +1,912 @@ +package testhelper + +// This file provides helper APIs to perform ports related operations. + +import ( + "fmt" + "strconv" + "strings" + "testing" + "time" + + 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 + 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 := strconv.Atoi(parentPortNumberStr) + if err != nil { + return 0, err + } + // Port ID is same as port index/parent port number for parent ports. + if isParent { + return uint32(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 + } + return uint32(laneIndex*512 + parentPortNumber + 1), nil + } + return uint32(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 := strconv.Atoi(values[0]) + if err != nil { + return nil, errors.Wrapf(err, "error parsing numBreakouts for breakout mode %v", mode) + } + u8numBreakouts := uint8(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 / uint8(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 <= 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) / 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) +} diff --git a/sdn_tests/pins_ondatra/infrastructure/testhelper/results.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/results.go new file mode 100644 index 00000000000..0c0f3fb8ba8 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/infrastructure/testhelper/ssh.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/ssh.go new file mode 100644 index 00000000000..137f0694e49 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/testhelper/ssh.go @@ -0,0 +1,136 @@ +package testhelper + +import ( + "fmt" + "time" + + "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: ssh.InsecureIgnoreHostKey(), + 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 +} + +// 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/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper.go b/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper.go new file mode 100644 index 00000000000..52c9a240655 --- /dev/null +++ b/sdn_tests/pins_ondatra/infrastructure/testhelper/testhelper.go @@ -0,0 +1,422 @@ +// Package testhelper contains APIs that help in writing GPINs Ondatra tests. +package testhelper + +import ( + "fmt" + "math/rand" + "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) + } + s := interfaces[rand.Intn(len(interfaces))] + + 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/sdn_tests/pins_ondatra/pins_deps.bzl b/sdn_tests/pins_ondatra/pins_deps.bzl new file mode 100644 index 00000000000..c834703736a --- /dev/null +++ b/sdn_tests/pins_ondatra/pins_deps.bzl @@ -0,0 +1,211 @@ +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("sonic_swss_common"): + # We use `git_repository` over `http_archive` only because this is a private repo + # requiring SSH authentication. + git_repository( + name = "sonic_swss_common", + commit = "672b1cfe1914489b79a91fff8a539b6c195f8959", + remote = "git@github.com:pins/sonic-swss-common.git", + ) + 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/sdn_tests/pins_ondatra/testrunner.sh b/sdn_tests/pins_ondatra/testrunner.sh new file mode 100755 index 00000000000..20503b8de4b --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/tests/BUILD.bazel b/sdn_tests/pins_ondatra/tests/BUILD.bazel new file mode 100644 index 00000000000..d76c8db4d65 --- /dev/null +++ b/sdn_tests/pins_ondatra/tests/BUILD.bazel @@ -0,0 +1,79 @@ +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"], +) + + +# 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 (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", + ], +) + +# 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", + ], +) + +# 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", + ], +) diff --git a/sdn_tests/pins_ondatra/tests/cpu_sw_single_switch_test.go b/sdn_tests/pins_ondatra/tests/cpu_sw_single_switch_test.go new file mode 100644 index 00000000000..5458a5b9cb8 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/tests/ethcounter_sw_dual_switch_test.go b/sdn_tests/pins_ondatra/tests/ethcounter_sw_dual_switch_test.go new file mode 100644 index 00000000000..2a81c27c73d --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/tests/gnmi_long_stress_test.go b/sdn_tests/pins_ondatra/tests/gnmi_long_stress_test.go new file mode 100644 index 00000000000..3140a7d9526 --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/tests/gnoi_file_test.go b/sdn_tests/pins_ondatra/tests/gnoi_file_test.go new file mode 100644 index 00000000000..9ae2a483a9e --- /dev/null +++ b/sdn_tests/pins_ondatra/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/sdn_tests/pins_ondatra/tests/inband_sw_interface_dual_switch_test.go b/sdn_tests/pins_ondatra/tests/inband_sw_interface_dual_switch_test.go new file mode 100644 index 00000000000..29c720b8fa2 --- /dev/null +++ b/sdn_tests/pins_ondatra/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 = "6.7.8.9" + 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("2.2.2.2").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("2.2.2.2").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/sdn_tests/pins_ondatra/tests/ondatra_test.bzl b/sdn_tests/pins_ondatra/tests/ondatra_test.bzl new file mode 100644 index 00000000000..55112e8cbaf --- /dev/null +++ b/sdn_tests/pins_ondatra/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 []) + ["//ondatra/data"] + testbed = testbed or "ondatra/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" : "ondatra/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"])