diff --git a/gnmi_server/platform_cli_test.go b/gnmi_server/platform_cli_test.go index c24fbbae..1105fde4 100644 --- a/gnmi_server/platform_cli_test.go +++ b/gnmi_server/platform_cli_test.go @@ -8,6 +8,8 @@ import ( "crypto/tls" "encoding/json" "fmt" + "os" + "strings" "testing" "time" @@ -872,6 +874,178 @@ func TestGetShowPlatformSsdhealth(t *testing.T) { } } +func TestGetShowPlatformPcieinfo(t *testing.T) { + showOutputFilename := "../testdata/PCIEINFO_SHOW.json" + checkOutputFilename := "../testdata/PCIEINFO_CHECK.json" + checkRawOutputFilename := "../testdata/PCIEINFO_CHECK_RAW.json" + invalidOutputFilename := "../testdata/INVALID_JSON.txt" + + showOutputBytes, err := os.ReadFile(showOutputFilename) + if err != nil { + t.Fatalf("read file %v err: %v", showOutputFilename, err) + } + checkOutputBytes, err := os.ReadFile(checkOutputFilename) + if err != nil { + t.Fatalf("read file %v err: %v", checkOutputFilename, err) + } + checkRawOutputBytes, err := os.ReadFile(checkRawOutputFilename) + if err != nil { + t.Fatalf("read file %v err: %v", checkRawOutputFilename, err) + } + + tests := []struct { + desc string + pathTarget string + textPbPath string + wantRetCode codes.Code + wantRespVal interface{} + valTest bool + testInit func() *gomonkey.Patches + }{ + { + desc: "query SHOW platform pcieinfo", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: showOutputBytes, + valTest: true, + testInit: func() *gomonkey.Patches { + return gomonkey.ApplyFunc(sccommon.GetDataFromHostCommand, func(cmd string) (string, error) { + if strings.Contains(cmd, "get_pcie_device") { + return string(showOutputBytes), nil + } + return "", fmt.Errorf("unexpected command: %s", cmd) + }) + }, + }, + { + desc: "query SHOW platform pcieinfo check", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: checkOutputBytes, + valTest: true, + testInit: func() *gomonkey.Patches { + return gomonkey.ApplyFunc(sccommon.GetDataFromHostCommand, func(cmd string) (string, error) { + if strings.Contains(cmd, "get_pcie_check") { + // Return raw output with extra fields (bus/dev/fn/id) as a real platform would. + // The handler must strip them and return only name/result. + return string(checkRawOutputBytes), nil + } + return "", fmt.Errorf("unexpected command: %s", cmd) + }) + }, + }, + { + desc: "query SHOW platform pcieinfo invalid JSON output", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + `, + wantRetCode: codes.NotFound, + wantRespVal: nil, + valTest: false, + testInit: func() *gomonkey.Patches { + return MockNSEnterOutput(t, invalidOutputFilename) + }, + }, + { + desc: "query SHOW platform pcieinfo host command error", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + `, + wantRetCode: codes.NotFound, + wantRespVal: nil, + valTest: false, + testInit: func() *gomonkey.Patches { + return gomonkey.ApplyFunc(sccommon.GetDataFromHostCommand, func(cmd string) (string, error) { + return "", fmt.Errorf("simulated command failure") + }) + }, + }, + { + desc: "query SHOW platform pcieinfo with verbose flag", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: showOutputBytes, + valTest: true, + testInit: func() *gomonkey.Patches { + return gomonkey.ApplyFunc(sccommon.GetDataFromHostCommand, func(cmd string) (string, error) { + if strings.Contains(cmd, "get_pcie_device") { + return string(showOutputBytes), nil + } + return "", fmt.Errorf("unexpected command: %s", cmd) + }) + }, + }, + { + desc: "query SHOW platform pcieinfo check with verbose flag", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: checkOutputBytes, + valTest: true, + testInit: func() *gomonkey.Patches { + return gomonkey.ApplyFunc(sccommon.GetDataFromHostCommand, func(cmd string) (string, error) { + if strings.Contains(cmd, "get_pcie_check") { + return string(checkRawOutputBytes), nil + } + return "", fmt.Errorf("unexpected command: %s", cmd) + }) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var patches *gomonkey.Patches + if tt.testInit != nil { + patches = tt.testInit() + } + defer func() { + if patches != nil { + patches.Reset() + } + }() + + s := createServer(t, ServerPort) + go runServer(t, s) + defer s.ForceStop() + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + opts := []grpc.DialOption{grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))} + + conn, err := grpc.Dial(TargetAddr, opts...) + if err != nil { + t.Fatalf("Dialing to %q failed: %v", TargetAddr, err) + } + defer conn.Close() + + gClient := pb.NewGNMIClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), QueryTimeout*time.Second) + defer cancel() + + runTestGet(t, ctx, gClient, tt.pathTarget, tt.textPbPath, tt.wantRetCode, tt.wantRespVal, tt.valTest) + }) + } +} + func TestGetShowPlatformSyseeprom(t *testing.T) { tests := []struct { desc string diff --git a/show_client/common/platform_apis.go b/show_client/common/platform_apis.go index f7d16684..a2a14c84 100644 --- a/show_client/common/platform_apis.go +++ b/show_client/common/platform_apis.go @@ -18,6 +18,21 @@ s = SsdUtil('%s') print(json.dumps({'model': str(s.get_model()), 'firmware': str(s.get_firmware()), 'serial': str(s.get_serial()), 'health': str(s.get_health()), 'temperature': str(s.get_temperature()), 'vendor_output': str(s.get_vendor_output())})) ` +// PcieInfoPyScript is the Python script template that loads the platform-specific +// or generic Pcie/PcieUtil and retrieves PCIe information as JSON. +// It expects two %s format parameters: platform path, then the API call (e.g., pcie.get_pcie_device() or pcie.get_pcie_check()). +var PcieInfoPyScript = ` +import json +platform_path = %s +try: + from sonic_platform.pcie import Pcie + pcie = Pcie(platform_path) +except ImportError: + from sonic_platform_base.sonic_pcie.pcie_common import PcieUtil + pcie = PcieUtil(platform_path) +print(json.dumps(%s)) +` + // ChassisComponentsPyScript retrieves all chassis components via Platform API var ChassisComponentsPyScript = ` import json diff --git a/show_client/platform_cli.go b/show_client/platform_cli.go index e181a968..799d0163 100644 --- a/show_client/platform_cli.go +++ b/show_client/platform_cli.go @@ -15,7 +15,8 @@ import ( ) const ( - chassisKey = "chassis 1" + chassisKey = "chassis 1" + pcieCheckSummaryName = "PCIe Device Checking All Test" ) // PlatformSummary represents the output structure for show platform summary @@ -124,6 +125,11 @@ type SsdHealthInfo struct { VendorOutput string `json:"vendor_output,omitempty"` } +type pcieCheckResult struct { + Name string `json:"name"` + Result string `json:"result"` +} + // getPlatformSummary implements the "show platform summary" command func getPlatformSummary(args sdc.CmdArgs, options sdc.OptionMap) ([]byte, error) { // Get version info to extract ASIC type @@ -543,3 +549,61 @@ func getPlatformSsdhealth(args sdc.CmdArgs, options sdc.OptionMap) ([]byte, erro return json.Marshal(ssdHealth) } + +func getPlatformPcieinfo(args sdc.CmdArgs, options sdc.OptionMap) ([]byte, error) { + apiCall := "pcie.get_pcie_device()" + if checkOpt, ok := options["check"].Bool(); ok && checkOpt { + apiCall = "pcie.get_pcie_check()" + } + + platformPath, _ := common.GetPathsToPlatformAndHwskuDirsOnHost() + + pyScript := fmt.Sprintf(common.PcieInfoPyScript, strconv.Quote(platformPath), apiCall) + escaped := strings.ReplaceAll(pyScript, "'", `'\''`) + pyCmd := fmt.Sprintf("python3 -c '%s'", escaped) + + output, err := common.GetDataFromHostCommand(pyCmd) + if err != nil { + trimmedOutput := strings.TrimSpace(output) + log.Errorf("Failed to get PCIe info from host command: %v, output: %s", err, trimmedOutput) + return nil, fmt.Errorf("failed to get PCIe info from host command: %w, output: %s", err, trimmedOutput) + } + + output = strings.TrimSpace(output) + if output == "" { + return []byte("[]"), nil + } + + if !json.Valid([]byte(output)) { + return nil, fmt.Errorf("invalid JSON output from PCIe host command: %s", strings.TrimSpace(output)) + } + + if checkOpt, ok := options["check"].Bool(); ok && checkOpt { + var normalized []pcieCheckResult + if err := json.Unmarshal([]byte(output), &normalized); err != nil { + return nil, fmt.Errorf("failed to parse PCIe check output: %w", err) + } + + overallResult := "Passed" + for i := range normalized { + if normalized[i].Result != "Passed" { + normalized[i].Result = "Failed" + overallResult = "Failed" + } + } + + normalized = append(normalized, pcieCheckResult{ + Name: pcieCheckSummaryName, + Result: overallResult, + }) + + normalizedBytes, err := json.Marshal(normalized) + if err != nil { + return nil, fmt.Errorf("failed to encode PCIe check output: %w", err) + } + + return normalizedBytes, nil + } + + return []byte(output), nil +} diff --git a/show_client/show_opts.go b/show_client/show_opts.go index 028ffc2a..3340233e 100644 --- a/show_client/show_opts.go +++ b/show_client/show_opts.go @@ -36,6 +36,7 @@ const ( showCmdOptionPsuIndexDesc = "[index=INTEGER] Display a specific PSU by index" showCmdOptionHistoryDesc = "[history=true] Display historical PFC statistics" showCmdOptionVendorDesc = "[vendor=true] Show vendor output (extended output if provided by platform vendor)" + showCmdOptionCheckDesc = "[check=true] Validate PCIe devices against expected list" ) // Option keys @@ -241,4 +242,10 @@ var ( showCmdOptionVendorDesc, sdc.BoolValue, ) + + showCmdOptionCheck = sdc.NewShowCmdOption( + "check", + showCmdOptionCheckDesc, + sdc.BoolValue, + ) ) diff --git a/show_client/show_paths.go b/show_client/show_paths.go index a55b7773..73bad77d 100644 --- a/show_client/show_paths.go +++ b/show_client/show_paths.go @@ -1185,6 +1185,18 @@ func init() { nil, ) + // SHOW/platform/pcieinfo + sdc.RegisterCliPath( + []string{"SHOW", "platform", "pcieinfo"}, + getPlatformPcieinfo, + "SHOW/platform/pcieinfo[OPTIONS]: Show device PCIe information", + 0, + 0, + nil, + showCmdOptionCheck, + showCmdOptionVerbose, + ) + // SHOW/platform/syseeprom sdc.RegisterCliPath( []string{"SHOW", "platform", "syseeprom"}, diff --git a/testdata/PCIEINFO_CHECK.json b/testdata/PCIEINFO_CHECK.json new file mode 100644 index 00000000..46e1d74d --- /dev/null +++ b/testdata/PCIEINFO_CHECK.json @@ -0,0 +1 @@ +[{"name":"PCI Device A","result":"Passed"},{"name":"PCI Device B","result":"Failed"},{"name":"PCIe Device Checking All Test","result":"Failed"}] diff --git a/testdata/PCIEINFO_CHECK_RAW.json b/testdata/PCIEINFO_CHECK_RAW.json new file mode 100644 index 00000000..e416c5d0 --- /dev/null +++ b/testdata/PCIEINFO_CHECK_RAW.json @@ -0,0 +1 @@ +[{"bus":"03","dev":"00","fn":"0","id":"1db6","name":"PCI Device A","result":"Passed"},{"bus":"03","dev":"01","fn":"0","id":"1db7","name":"PCI Device B","result":"Failed"}] diff --git a/testdata/PCIEINFO_SHOW.json b/testdata/PCIEINFO_SHOW.json new file mode 100644 index 00000000..45a359c4 --- /dev/null +++ b/testdata/PCIEINFO_SHOW.json @@ -0,0 +1 @@ +[{"bus":"03","dev":"00","fn":"0","id":"1db6","name":"Mellanox Device"}]