From f36af190594cc3cdc52dabe0c61395793a05eede Mon Sep 17 00:00:00 2001 From: marcushines <80116818+marcushines@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:37:35 -0700 Subject: [PATCH] Add core handler to binding (#2296) --- go.mod | 6 +- go.sum | 11 +- internal/core/core.go | 266 ++++++++++++++++++++ internal/core/core_test.go | 492 +++++++++++++++++++++++++++++++++++++ topologies/binding/new.go | 8 +- 5 files changed, 773 insertions(+), 10 deletions(-) create mode 100644 internal/core/core.go create mode 100644 internal/core/core_test.go diff --git a/go.mod b/go.mod index 9d97ec5c823..2bfdd76a29d 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( cloud.google.com/go/iam v1.1.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect - github.com/aristanetworks/arista-ceoslab-operator/v2 v2.0.1 // indirect + github.com/aristanetworks/arista-ceoslab-operator/v2 v2.0.2 // indirect github.com/carlmontanari/difflibgo v0.0.0-20210718194309-31b9e131c298 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cloudflare/circl v1.3.3 // indirect @@ -99,7 +99,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/networkop/meshnet-cni v0.3.1-0.20230525201116-d7c306c635cf // indirect - github.com/open-traffic-generator/ixia-c-operator v0.3.4 // indirect + github.com/open-traffic-generator/ixia-c-operator v0.3.6 // indirect github.com/openconfig/grpctunnel v0.0.0-20220819142823-6f5422b8ca70 // indirect github.com/openconfig/lemming/operator v0.2.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect @@ -110,7 +110,7 @@ require ( github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/scrapli/scrapligo v1.1.7 // indirect + github.com/scrapli/scrapligo v1.1.11 // indirect github.com/scrapli/scrapligocfg v1.0.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 // indirect diff --git a/go.sum b/go.sum index 83f23c2e778..b12133d3fd5 100644 --- a/go.sum +++ b/go.sum @@ -755,8 +755,8 @@ github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0I github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= -github.com/aristanetworks/arista-ceoslab-operator/v2 v2.0.1 h1:lOGhvrrxc/bzUMJI4kYmZh/N1a87Vnsk/TENZt5kCoc= -github.com/aristanetworks/arista-ceoslab-operator/v2 v2.0.1/go.mod h1:/mvSt2fEmlVEU7dppip3UNz/MUt380f50dFsZRGn83o= +github.com/aristanetworks/arista-ceoslab-operator/v2 v2.0.2 h1:KQL1evr4NM4ZQOLRs1bbmD0kYPmLRAMqvRrNSpYAph4= +github.com/aristanetworks/arista-ceoslab-operator/v2 v2.0.2/go.mod h1:/mvSt2fEmlVEU7dppip3UNz/MUt380f50dFsZRGn83o= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -1132,8 +1132,8 @@ github.com/onsi/ginkgo/v2 v2.6.0 h1:9t9b9vRUbFq3C4qKFCGkVuq/fIHji802N1nrtkh1mNc= github.com/onsi/ginkgo/v2 v2.6.0/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc= github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/open-traffic-generator/ixia-c-operator v0.3.4 h1:xH0hLVWf2wuVUT9ovFdh/WwwBf1oGkBu5YEWD61igck= -github.com/open-traffic-generator/ixia-c-operator v0.3.4/go.mod h1:Q+ZXCinXxUKcnrJf5PJC1Q7JxUQc5ZPZA85jwVAqIRQ= +github.com/open-traffic-generator/ixia-c-operator v0.3.6 h1:dablUs6FAToVDFaoIo2M+Z9UCa93KAwlj7HJqNwLwTQ= +github.com/open-traffic-generator/ixia-c-operator v0.3.6/go.mod h1:Q+ZXCinXxUKcnrJf5PJC1Q7JxUQc5ZPZA85jwVAqIRQ= github.com/open-traffic-generator/snappi/gosnappi v0.13.0 h1:RdlbT+CIlVum6xbhiFr/IzTvQee5bMa3V4oBWa79UBw= github.com/open-traffic-generator/snappi/gosnappi v0.13.0/go.mod h1:QjB939WFJqUq6V7RQqkY/LFCgRRzKrybHHFp7F7xdWA= github.com/openconfig/entity-naming v0.0.0-20230912181021-7ac806551a31 h1:K/9O+J20+liIof8WjquMydnebD0N1U9ItjhJYF6H4hg= @@ -1242,8 +1242,9 @@ github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/scrapli/scrapligo v1.0.0/go.mod h1:jvRMdb90MNnswMiku8UNXj8JZaOIPhwhcqqFwr9qeoY= -github.com/scrapli/scrapligo v1.1.7 h1:xc0/bTDT+BfLkjJ3B4X6/8lxuzW7tgB8BMg8Tzn1yHQ= github.com/scrapli/scrapligo v1.1.7/go.mod h1:rRx/rT2oNPYztiT3/ik0FRR/Ro7AdzN/eR9AtF8A81Y= +github.com/scrapli/scrapligo v1.1.11 h1:ATvpF2LDoxnd/HlfSj5A0IiJDro75D6nuCx8m6S44vU= +github.com/scrapli/scrapligo v1.1.11/go.mod h1:XrSom4Gd87B110QkyTaTkuL2EbzEVOlgCJGKIZa6wns= github.com/scrapli/scrapligocfg v1.0.0 h1:540SuGqqM6rKN87SLCfR54IageQ6s3a/ZOycGRgbbak= github.com/scrapli/scrapligocfg v1.0.0/go.mod h1:9+6k9dQeIqEZEg6EK5YXEjuVb7h+nvvel26CY1RGjy4= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= diff --git a/internal/core/core.go b/internal/core/core.go new file mode 100644 index 00000000000..1ea6f8af562 --- /dev/null +++ b/internal/core/core.go @@ -0,0 +1,266 @@ +// Copyright 2023 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. + +// Package core provides a validator for being able to +// check for core files on DUT's before and after test +// modules runs. +package core + +import ( + "bytes" + "context" + "fmt" + "regexp" + "sync" + "text/template" + "time" + + "github.com/golang/glog" + "github.com/openconfig/ondatra" + "github.com/openconfig/ondatra/binding" + "github.com/openconfig/ondatra/eventlis" + "google.golang.org/grpc" + + fpb "github.com/openconfig/gnoi/file" + opb "github.com/openconfig/ondatra/proto" +) + +var ( + vendorCoreFilePath = map[opb.Device_Vendor]string{ + opb.Device_JUNIPER: "/var/core/", + opb.Device_CISCO: "/misc/disk1/", + opb.Device_NOKIA: "/var/core/", + opb.Device_ARISTA: "/var/core/", + } + vendorCoreFileNamePattern = map[opb.Device_Vendor]*regexp.Regexp{ + opb.Device_JUNIPER: regexp.MustCompile(".*.tar.gz"), + opb.Device_CISCO: regexp.MustCompile("/misc/disk1/.*core.*"), + opb.Device_NOKIA: regexp.MustCompile("/var/core/coredump-.*"), + opb.Device_ARISTA: regexp.MustCompile("/var/core/core.*"), + } +) + +var ( + validator validatorImpl +) + +type fileInfo struct { + Name string + Path string + Modified uint64 +} + +type dutCoreFiles struct { + DUT string + Files coreFiles + Status string +} + +type coreFiles map[string]fileInfo + +type checker struct { + dut binding.DUT + fileClient fpb.FileClient + + mu sync.Mutex + startTime time.Time + endTime time.Time + prevCores coreFiles +} + +func newChecker(dut binding.DUT) (*checker, error) { + dutVendor := dut.Vendor() + // vendorCoreFilePath and vendorCoreProcName should be provided to fetch core file on dut. + if _, ok := vendorCoreFilePath[dutVendor]; !ok { + return nil, fmt.Errorf("add support for vendor %v in var vendorCoreFilePath", dutVendor) + } + if _, ok := vendorCoreFileNamePattern[dutVendor]; !ok { + return nil, fmt.Errorf("add support for vendor %v in var vendorCoreFileNamePattern", dutVendor) + } + gClients, err := dut.DialGNOI(context.Background(), grpc.WithBlock()) + if err != nil { + return nil, err + } + return &checker{ + dut: dut, + fileClient: gClients.File(), + prevCores: coreFiles{}, + startTime: time.Now(), + }, nil +} + +func (c *checker) check() (coreFiles, error) { + c.mu.Lock() + defer c.mu.Unlock() + cores, err := c.checkCores() + if err != nil { + return nil, err + } + delta := coreFiles{} + for k, v := range cores { + if _, ok := c.prevCores[k]; !ok { + delta[k] = v + } + } + c.prevCores = cores + return delta, nil +} + +type validatorImpl struct { + mu sync.Mutex + duts map[string]*checker +} + +func (v *validatorImpl) check() map[string]dutCoreFiles { + var wg sync.WaitGroup + var mu sync.Mutex + dutCores := map[string]dutCoreFiles{} + for _, c := range v.duts { + wg.Add(1) + go func(c *checker) { + defer wg.Done() + cores, err := c.check() + status := "OK" + if err != nil { + status = fmt.Sprintf("DUT %q failed to check cores: %v", c.dut.Name(), err) + glog.Warning(status) + } + mu.Lock() + defer mu.Unlock() + dutCores[c.dut.Name()] = dutCoreFiles{ + DUT: c.dut.Name(), + Files: cores, + Status: status, + } + }(c) + } + wg.Wait() + return dutCores +} + +// start starts a core file watcher for the provided DUT. +func (v *validatorImpl) start(duts map[string]binding.DUT) map[string]dutCoreFiles { + v.mu.Lock() + defer v.mu.Unlock() + for k, dut := range duts { + glog.Infof("Registering core file checking for DUT %q", k) + c, err := newChecker(dut) + if err != nil { + glog.Warningf("Failed to register core file checking for DUT %q: %v", k, err) + continue + } + v.duts[k] = c + } + return v.check() +} + +// Stop ends the validator and returns a list of all DUTs that +// found core files. +func (v *validatorImpl) stop() map[string]dutCoreFiles { + v.mu.Lock() + defer v.mu.Unlock() + return v.check() +} + +func registerBefore(e *eventlis.BeforeTestsEvent) error { + cores := validator.start(e.Reservation.DUTs) + ondatra.Report().AddSuiteProperty("validator.core", "enabled") + report := createReport(cores) + ondatra.Report().AddSuiteProperty("validator.core.initial", report) + return nil +} + +const ( + coreFmt = ` +Delta Core Files by DUT:{{range $key, $dut := .}} +DUT: {{$key}}{{ range $key, $cores := $dut.Files }} + {{ $key }}{{ end }}{{ end }}` +) + +var coreTemplate = template.Must(template.New("errorMsg").Parse(coreFmt)) + +func createReport(d map[string]dutCoreFiles) string { + b := new(bytes.Buffer) + if err := coreTemplate.Execute(b, d); err != nil { + b.Reset() + fmt.Fprintf(b, "parse error on retrieving core files: %v", err) + } + return b.String() +} + +func registerAfter(e *eventlis.AfterTestsEvent) error { + cores := validator.stop() + foundCores := false + for _, files := range cores { + if len(files.Files) > 0 { + foundCores = true + break + } + } + report := createReport(cores) + msg := fmt.Sprintf("core file check found cores:\n%s", report) + glog.Infof(msg) + ondatra.Report().AddSuiteProperty("validator.core.end", report) + if foundCores { + return fmt.Errorf(msg) + } + return nil +} + +// Register will register core file watcher with the caller. +// This will allow the event listener to fire on test module start and end. +// All DUTs in the reservation will be monitored. +func Register() { + validator = validatorImpl{ + duts: map[string]*checker{}, + } + ondatra.EventListener().AddBeforeTestsCallback(registerBefore) + ondatra.EventListener().AddAfterTestsCallback(registerAfter) +} + +// coreFileCheck function is used to check if cores are found on the DUT. +func (c *checker) checkCores() (coreFiles, error) { + dutVendor := c.dut.Vendor() + corePath := vendorCoreFilePath[dutVendor] + fileMatch := vendorCoreFileNamePattern[dutVendor] + in := &fpb.StatRequest{ + Path: corePath, + } + validResponse, err := c.fileClient.Stat(context.Background(), in) + if err != nil { + return nil, fmt.Errorf("DUT %q: %w", corePath, err) + } + cores := coreFiles{} + // Check cores creation time is greater than test start time. + for _, fileStatsInfo := range validResponse.GetStats() { + // Get the exact file. + in = &fpb.StatRequest{ + Path: fileStatsInfo.GetPath(), + } + validResponse, err := c.fileClient.Stat(context.Background(), in) + if err != nil { + return nil, fmt.Errorf("DUT %q: unable to stat file %q, %v", c.dut.Name(), fileStatsInfo.GetPath(), err) + } + for _, filesMatched := range validResponse.GetStats() { + coreFileName := filesMatched.GetPath() + if fileMatch.MatchString(coreFileName) { + cores[coreFileName] = fileInfo{ + Name: coreFileName, + Modified: fileStatsInfo.GetLastModified(), + } + } + } + } + return cores, nil +} diff --git a/internal/core/core_test.go b/internal/core/core_test.go new file mode 100644 index 00000000000..ba93035cc10 --- /dev/null +++ b/internal/core/core_test.go @@ -0,0 +1,492 @@ +// Copyright 2023 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. + +package core + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/openconfig/gnmi/errdiff" + "github.com/openconfig/ondatra/binding" + "github.com/openconfig/ondatra/eventlis" + "github.com/openconfig/ondatra/fakebind" + "google.golang.org/grpc" + + fpb "github.com/openconfig/gnoi/file" + opb "github.com/openconfig/ondatra/proto" +) + +type fakeGNOI struct { + *binding.AbstractGNOIClients + fakeFileClient *fakeFileClient +} + +func (f *fakeGNOI) File() fpb.FileClient { + return f.fakeFileClient +} + +type fakeFileClient struct { + fpb.FileClient + statResponses []any +} + +func (f *fakeFileClient) Stat(ctx context.Context, in *fpb.StatRequest, opts ...grpc.CallOption) (*fpb.StatResponse, error) { + if len(f.statResponses) == 0 { + return nil, fmt.Errorf("no more responses") + } + resp := f.statResponses[0] + f.statResponses = f.statResponses[1:] + switch v := resp.(type) { + case *fpb.StatResponse: + return v, nil + case error: + return nil, v + } + return nil, fmt.Errorf("invalid response type: %T", resp) +} + +func TestCoreValidator(t *testing.T) { + tests := []struct { + desc string + duts map[string]binding.DUT + startErr string + stopErr string + cores map[string]dutCoreFiles + startCores map[string]dutCoreFiles + }{{ + desc: "invalid dut vendor", + duts: map[string]binding.DUT{ + "dut1": &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_VENDOR_UNSPECIFIED, + Name: "dut1", + }, + }, + }, + }, + startCores: map[string]dutCoreFiles{}, + cores: map[string]dutCoreFiles{}, + }, { + desc: "dut gnoi error", + duts: map[string]binding.DUT{ + "dut1": &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return nil, fmt.Errorf("gnoi dial failed") + }, + }, + }, + startCores: map[string]dutCoreFiles{}, + cores: map[string]dutCoreFiles{}, + }, { + desc: "dut gnoi rpc match stat fail", + duts: map[string]binding.DUT{ + "dut1": &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + fmt.Errorf("gnoi.File.Stat failed"), + fmt.Errorf("gnoi.File.Stat failed"), + fmt.Errorf("gnoi.File.Stat failed"), + fmt.Errorf("gnoi.File.Stat failed"), + }, + }, + }, nil + }, + }, + }, + startCores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Status: `DUT "dut1" failed to check cores: DUT "/var/core/": gnoi.File.Stat failed`, + }, + }, + cores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Status: `DUT "dut1" failed to check cores: DUT "/var/core/": gnoi.File.Stat failed`, + }, + }, + }, { + desc: "dut gnoi rpc file stat failed", + duts: map[string]binding.DUT{ + "dut1": &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + fmt.Errorf("gnoi.File.Stat failed"), + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + fmt.Errorf("gnoi.File.Stat failed"), + }, + }, + }, nil + }, + }, + }, + startCores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Status: `DUT "dut1" failed to check cores: DUT "dut1": unable to stat file "/var/core/core.1.tar.gz", gnoi.File.Stat failed`, + }, + }, + cores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Status: `DUT "dut1" failed to check cores: DUT "dut1": unable to stat file "/var/core/core.1.tar.gz", gnoi.File.Stat failed`, + }, + }, + }, { + desc: "dut gnoi pass no delta", + duts: map[string]binding.DUT{ + "dut1": &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + }, + }, + }, nil + }, + }, + }, + startCores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Files: coreFiles{ + "/var/core/core.1.tar.gz": fileInfo{ + Name: "/var/core/core.1.tar.gz", + }, + }, + Status: "OK", + }, + }, + cores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Files: coreFiles{}, + Status: "OK", + }, + }, + }, { + desc: "dut gnoi pass delta", + duts: map[string]binding.DUT{ + "dut1": &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }, { + Path: "/var/core/core.2.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.2.tar.gz", + }}, + }, + }, + }, + }, nil + }, + }, + }, + cores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Files: coreFiles{ + "/var/core/core.2.tar.gz": fileInfo{ + Name: "/var/core/core.2.tar.gz", + }, + }, + Status: "OK", + }, + }, + startCores: map[string]dutCoreFiles{ + "dut1": { + DUT: "dut1", + Files: coreFiles{ + "/var/core/core.1.tar.gz": fileInfo{ + Name: "/var/core/core.1.tar.gz", + }, + }, + Status: "OK", + }, + }, + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + validator = validatorImpl{ + duts: map[string]*checker{}, + } + cores := validator.start(tt.duts) + if s := cmp.Diff(cores, tt.startCores); s != "" { + t.Fatalf("Start(%+v) core check failed: %s", tt.duts, s) + } + cores = validator.stop() + if s := cmp.Diff(cores, tt.cores); s != "" { + t.Fatalf("Stop() core check failed: %s", s) + } + }) + } +} + +func TestEventCallback(t *testing.T) { + tests := []struct { + desc string + dut *fakebind.DUT + beforeErr string + afterErr string + }{{ + desc: "Fail to register (this will only log error)", + dut: &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + fmt.Errorf("gnoi.File.Stat failed"), + }, + }, + }, nil + }, + }, + }, { + desc: "Fail on stop (this will also be ignored)", + dut: &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + fmt.Errorf("gnoi.File.Stat failed"), + }, + }, + }, nil + }, + }, + }, { + desc: "After returns no new core", + dut: &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + }, + }, + }, nil + }, + }, + }, { + desc: "After returns error for core found", + dut: &fakebind.DUT{ + AbstractDUT: &binding.AbstractDUT{ + Dims: &binding.Dims{ + Vendor: opb.Device_ARISTA, + Name: "dut1", + }, + }, + DialGNOIFn: func(_ context.Context, _ ...grpc.DialOption) (binding.GNOIClients, error) { + return &fakeGNOI{ + fakeFileClient: &fakeFileClient{ + statResponses: []any{ + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }, { + Path: "/var/core/core.2.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.1.tar.gz", + }}, + }, + &fpb.StatResponse{ + Stats: []*fpb.StatInfo{{ + Path: "/var/core/core.2.tar.gz", + }}, + }, + }, + }, + }, nil + }, + }, + afterErr: `Delta Core Files by DUT: +DUT: dut1 + /var/core/core.2.tar.gz`, + }} + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + validator = validatorImpl{ + duts: map[string]*checker{}, + } + e := &eventlis.BeforeTestsEvent{ + Reservation: &binding.Reservation{ + DUTs: map[string]binding.DUT{ + tt.dut.Name(): tt.dut, + }, + }, + } + beforeErr := registerBefore(e) + if s := errdiff.Check(beforeErr, tt.beforeErr); s != "" { + t.Fatalf("registerBefore failed: %v", s) + } + aE := &eventlis.AfterTestsEvent{ + ExitCode: new(int), + } + afterErr := registerAfter(aE) + if s := errdiff.Check(afterErr, tt.afterErr); s != "" { + t.Fatalf("registerAfter failed: %v", s) + } + }) + + } +} diff --git a/topologies/binding/new.go b/topologies/binding/new.go index c9e9626ea53..a16c83c1fb4 100644 --- a/topologies/binding/new.go +++ b/topologies/binding/new.go @@ -25,14 +25,16 @@ import ( "flag" "github.com/golang/glog" + "github.com/openconfig/featureprofiles/internal/core" "github.com/openconfig/featureprofiles/internal/rundata" - bindpb "github.com/openconfig/featureprofiles/topologies/proto/binding" "github.com/openconfig/ondatra" "github.com/openconfig/ondatra/binding" "github.com/openconfig/ondatra/knebind" knecreds "github.com/openconfig/ondatra/knebind/creds" - opb "github.com/openconfig/ondatra/proto" "google.golang.org/protobuf/encoding/prototext" + + bindpb "github.com/openconfig/featureprofiles/topologies/proto/binding" + opb "github.com/openconfig/ondatra/proto" ) var ( @@ -71,6 +73,8 @@ func New() (binding.Binding, error) { if err != nil { return nil, err } + // Register core file handler for DUTs. + core.Register() return &rundataBind{Binding: b}, nil }