diff --git a/gnmi_server/platform_firmware_cli_test.go b/gnmi_server/platform_firmware_cli_test.go new file mode 100644 index 00000000..2c69d91f --- /dev/null +++ b/gnmi_server/platform_firmware_cli_test.go @@ -0,0 +1,412 @@ +package gnmi + +import ( + "crypto/tls" + "fmt" + "strings" + "testing" + "time" + + "github.com/agiledragon/gomonkey/v2" + pb "github.com/openconfig/gnmi/proto/gnmi" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + + "github.com/sonic-net/sonic-gnmi/show_client/common" + "github.com/sonic-net/sonic-gnmi/show_client/helpers" +) + +func TestGetShowPlatformFirmwareStatus(t *testing.T) { + // Expected output matching actual device output (MSN2700) + expectedOutput := `{"chassis":"MSN2700","module":"N/A","components":[{"name":"ONIE","version":"2018.05-5.2.0004-9600","description":"ONIE - Open Network Install Environment"},{"name":"SSD","version":"0115-000","description":"SSD - Solid-State Drive"},{"name":"BIOS","version":"0ABZS017_02.02.002","description":"BIOS - Basic Input/Output System"},{"name":"CPLD1","version":"CPLD000085_REV2000","description":"CPLD - Complex Programmable Logic Device"},{"name":"CPLD2","version":"CPLD000128_REV0600","description":"CPLD - Complex Programmable Logic Device"},{"name":"CPLD3","version":"CPLD000000_REV0300","description":"CPLD - Complex Programmable Logic Device"}]}` + + // Expected output for modular chassis test + expectedModularOutput := `{"chassis":"ModularChassis","module":"Module1","components":[{"name":"BIOS","version":"1.0.0","description":"System BIOS"},{"name":"FPGA","version":"2.1.0","description":"Module FPGA"},{"name":"CPLD","version":"3.0.0","description":"Module CPLD"}]}` + + expectedEmptyOutput := `{"chassis":"N/A","module":"N/A","components":[]}` + + tests := []struct { + desc string + pathTarget string + textPbPath string + wantRetCode codes.Code + wantRespVal interface{} + valTest bool + testInit func() func() + }{ + { + desc: "query SHOW platform firmware status success", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(expectedOutput), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Mock helper functions for MSN2700 non-modular chassis + patches := gomonkey.NewPatches() + + // Mock GetAllFirmwareData to return MSN2700 components + patches.ApplyFunc(helpers.GetAllFirmwareData, func() ([]helpers.FirmwareData, error) { + return []helpers.FirmwareData{ + { + Chassis: "MSN2700", + Module: "N/A", + Component: "ONIE", + Version: "2018.05-5.2.0004-9600", + Description: "ONIE - Open Network Install Environment", + }, + { + Chassis: "", + Module: "", + Component: "SSD", + Version: "0115-000", + Description: "SSD - Solid-State Drive", + }, + { + Chassis: "", + Module: "", + Component: "BIOS", + Version: "0ABZS017_02.02.002", + Description: "BIOS - Basic Input/Output System", + }, + { + Chassis: "", + Module: "", + Component: "CPLD1", + Version: "CPLD000085_REV2000", + Description: "CPLD - Complex Programmable Logic Device", + }, + { + Chassis: "", + Module: "", + Component: "CPLD2", + Version: "CPLD000128_REV0600", + Description: "CPLD - Complex Programmable Logic Device", + }, + { + Chassis: "", + Module: "", + Component: "CPLD3", + Version: "CPLD000000_REV0300", + Description: "CPLD - Complex Programmable Logic Device", + }, + }, nil + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "query SHOW platform firmware status modular chassis", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(expectedModularOutput), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Mock helper function for modular chassis + patches := gomonkey.NewPatches() + + patches.ApplyFunc(helpers.GetAllFirmwareData, func() ([]helpers.FirmwareData, error) { + return []helpers.FirmwareData{ + { + Chassis: "ModularChassis", + Module: "", + Component: "BIOS", + Version: "1.0.0", + Description: "System BIOS", + }, + { + Chassis: "", + Module: "Module1", + Component: "FPGA", + Version: "2.1.0", + Description: "Module FPGA", + }, + { + Chassis: "", + Module: "", + Component: "CPLD", + Version: "3.0.0", + Description: "Module CPLD", + }, + }, nil + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "query SHOW platform firmware status no components", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(expectedEmptyOutput), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Mock helper function returning empty data + patches := gomonkey.NewPatches() + + patches.ApplyFunc(helpers.GetAllFirmwareData, func() ([]helpers.FirmwareData, error) { + return []helpers.FirmwareData{}, nil + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "test helper functions coverage - chassis name and components", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"chassis":"TestChassis","module":"N/A","components":[{"name":"BIOS","version":"1.0.0","description":"Test BIOS"},{"name":"CPLD","version":"2.0.0","description":"Test CPLD"}]}`), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Add test data for CHASSIS_INFO to STATE_DB + stateDbClient := getRedisClientN(t, StateDbNum, "") + stateDbClient.HSet(context.Background(), "CHASSIS_INFO|chassis 1", "model", "TestChassis") + stateDbClient.Close() + + // Mock individual helper functions to test integration logic + patches := gomonkey.NewPatches() + + patches.ApplyFunc(helpers.GetChassisComponents, func() ([]helpers.ComponentInfo, error) { + return []helpers.ComponentInfo{ + { + Name: "BIOS", + FirmwareVersion: "1.0.0", + Description: "Test BIOS", + }, + { + Name: "CPLD", + FirmwareVersion: "2.0.0", + Description: "Test CPLD", + }, + }, nil + }) + + patches.ApplyFunc(helpers.GetModuleComponents, func() ([]helpers.ModuleComponentInfo, error) { + // Return empty for non-modular chassis + return []helpers.ModuleComponentInfo{}, nil + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "test helper functions coverage - modular chassis with modules", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"chassis":"Modular","module":"LineCard1","components":[{"name":"BIOS","version":"1.0","description":"Chassis BIOS"},{"name":"FPGA","version":"2.0","description":"Module FPGA"},{"name":"CPLD","version":"3.0","description":"Module CPLD"}]}`), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Add test data for CHASSIS_INFO to STATE_DB + stateDbClient := getRedisClientN(t, StateDbNum, "") + stateDbClient.HSet(context.Background(), "CHASSIS_INFO|chassis 1", "model", "Modular") + stateDbClient.Close() + + // Mock helper functions for modular chassis with modules + patches := gomonkey.NewPatches() + + patches.ApplyFunc(helpers.GetChassisComponents, func() ([]helpers.ComponentInfo, error) { + return []helpers.ComponentInfo{ + { + Name: "BIOS", + FirmwareVersion: "1.0", + Description: "Chassis BIOS", + }, + }, nil + }) + + patches.ApplyFunc(helpers.GetModuleComponents, func() ([]helpers.ModuleComponentInfo, error) { + return []helpers.ModuleComponentInfo{ + { + ModuleName: "LineCard1", + Name: "FPGA", + FirmwareVersion: "2.0", + Description: "Module FPGA", + }, + { + ModuleName: "LineCard1", + Name: "CPLD", + FirmwareVersion: "3.0", + Description: "Module CPLD", + }, + }, nil + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "test helper functions error handling - platform API failures", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(expectedEmptyOutput), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Mock helper functions returning errors to test error handling + patches := gomonkey.NewPatches() + + // Mock database query failure for chassis info + patches.ApplyFunc(common.GetMapFromQueries, func(queries [][]string) (map[string]interface{}, error) { + return nil, fmt.Errorf("database query error") + }) + + patches.ApplyFunc(helpers.GetChassisComponents, func() ([]helpers.ComponentInfo, error) { + return nil, fmt.Errorf("chassis components error") + }) + + patches.ApplyFunc(helpers.GetModuleComponents, func() ([]helpers.ModuleComponentInfo, error) { + return nil, fmt.Errorf("module components error") + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "test helper functions coverage - platform API command failures", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(expectedEmptyOutput), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Mock Platform API command failures to test error handling + patches := gomonkey.NewPatches() + + patches.ApplyFunc(common.GetDataFromHostCommand, func(command string) (string, error) { + // All Platform API calls fail + return "", fmt.Errorf("platform API error") + }) + + return func() { + patches.Reset() + } + }, + }, + { + desc: "test helper functions coverage - invalid JSON response", + pathTarget: "SHOW", + textPbPath: ` + elem: + elem: + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(expectedEmptyOutput), + valTest: true, + testInit: func() func() { + ResetDataSetsAndMappings(t) + + // Mock invalid JSON responses to test parsing error handling + patches := gomonkey.NewPatches() + + patches.ApplyFunc(common.GetDataFromHostCommand, func(command string) (string, error) { + // Return invalid JSON to test error handling in GetChassisComponents and GetModuleComponents + if strings.Contains(command, "json.dumps") { + return "invalid json}", nil + } + // Return valid chassis name + return "TestChassis", nil + }) + + return func() { + patches.Reset() + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + var cleanup func() + if tt.testInit != nil { + cleanup = tt.testInit() + } + defer func() { + if cleanup != nil { + cleanup() + } + }() + + 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) + }) + } +} diff --git a/show_client/common/platform_apis.go b/show_client/common/platform_apis.go index 15756e46..f7d16684 100644 --- a/show_client/common/platform_apis.go +++ b/show_client/common/platform_apis.go @@ -18,6 +18,62 @@ 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())})) ` +// ChassisComponentsPyScript retrieves all chassis components via Platform API +var ChassisComponentsPyScript = ` +import json +try: + from sonic_platform.platform import Platform + chassis = Platform().get_chassis() + components = [] + + if hasattr(chassis, 'get_all_components'): + for component in chassis.get_all_components(): + try: + components.append({ + 'name': component.get_name() if hasattr(component, 'get_name') else 'N/A', + 'firmware_version': component.get_firmware_version() if hasattr(component, 'get_firmware_version') else 'N/A', + 'description': component.get_description() if hasattr(component, 'get_description') else 'N/A' + }) + except Exception: + continue + + print(json.dumps(components)) +except Exception: + print('[]') +` + +// ModuleComponentsPyScript retrieves all module components via Platform API +var ModuleComponentsPyScript = ` +import json +try: + from sonic_platform.platform import Platform + chassis = Platform().get_chassis() + components = [] + + if hasattr(chassis, 'get_all_modules'): + for module in chassis.get_all_modules(): + try: + module_name = module.get_name() if hasattr(module, 'get_name') else 'N/A' + + if hasattr(module, 'get_all_components'): + for component in module.get_all_components(): + try: + components.append({ + 'module_name': module_name, + 'name': component.get_name() if hasattr(component, 'get_name') else 'N/A', + 'firmware_version': component.get_firmware_version() if hasattr(component, 'get_firmware_version') else 'N/A', + 'description': component.get_description() if hasattr(component, 'get_description') else 'N/A' + }) + except Exception: + continue + except Exception: + continue + + print(json.dumps(components)) +except Exception: + print('[]') +` + // SysEepromPyScript is the Python script that invokes the sonic_platform API // to retrieve system EEPROM info. var SysEepromPyScript = ` diff --git a/show_client/helpers/platform_firmware_helper.go b/show_client/helpers/platform_firmware_helper.go new file mode 100644 index 00000000..6f8a7659 --- /dev/null +++ b/show_client/helpers/platform_firmware_helper.go @@ -0,0 +1,176 @@ +package helpers + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/sonic-net/sonic-gnmi/show_client/common" +) + +// ComponentInfo holds component data from Platform API +type ComponentInfo struct { + Name string + FirmwareVersion string + Description string +} + +// ModuleComponentInfo holds module component data +type ModuleComponentInfo struct { + ModuleName string + Name string + FirmwareVersion string + Description string +} + +// FirmwareData holds complete firmware information for a component +type FirmwareData struct { + Chassis string + Module string + Component string + Version string + Description string +} + +// GetAllFirmwareData retrieves complete firmware information using Platform API +func GetAllFirmwareData() ([]FirmwareData, error) { + firmwareList := make([]FirmwareData, 0) + + // Get chassis info from database + chassisInfo, err := common.GetChassisInfo() + chassisName := "N/A" + if err == nil { + chassisName = chassisInfo["model"] + } + + // Check if modular chassis to determine module name logic + isModularChassis := false + moduleComponents, moduleErr := GetModuleComponents() + if moduleErr == nil && len(moduleComponents) > 0 { + isModularChassis = true + } + + appendChassisName := true + appendModuleNA := !isModularChassis // Show "N/A" for non-modular chassis + + // Get chassis components + chassisComponents, err := GetChassisComponents() + if err == nil { + for _, component := range chassisComponents { + moduleField := "" + if appendModuleNA { + moduleField = "N/A" + appendModuleNA = false + } + + firmware := FirmwareData{ + Chassis: func() string { + if appendChassisName { + appendChassisName = false + return chassisName + } + return "" + }(), + Module: moduleField, + Component: component.Name, + Version: component.FirmwareVersion, + Description: component.Description, + } + firmwareList = append(firmwareList, firmware) + } + } + + // Get module components for modular chassis + if isModularChassis { + currentModuleName := "" + appendModuleName := false + + for _, moduleComp := range moduleComponents { + // New module - show module name for first component + if moduleComp.ModuleName != currentModuleName { + currentModuleName = moduleComp.ModuleName + appendModuleName = true + } + + moduleNameField := "" + if appendModuleName { + moduleNameField = moduleComp.ModuleName + appendModuleName = false + } + + firmware := FirmwareData{ + Chassis: func() string { + if appendChassisName { + appendChassisName = false + return chassisName + } + return "" + }(), + Module: moduleNameField, + Component: moduleComp.Name, + Version: moduleComp.FirmwareVersion, + Description: moduleComp.Description, + } + firmwareList = append(firmwareList, firmware) + } + } + + return firmwareList, nil +} + +// GetChassisComponents calls Platform API to get chassis components +func GetChassisComponents() ([]ComponentInfo, error) { + escaped := strings.ReplaceAll(common.ChassisComponentsPyScript, "'", `'\''`) + command := fmt.Sprintf("python3 -c '%s'", escaped) + + output, err := common.GetDataFromHostCommand(command) + if err != nil { + return nil, err + } + + var rawComponents []map[string]string + if err := json.Unmarshal([]byte(output), &rawComponents); err != nil { + return nil, err + } + + // Parse into Go structs + components := make([]ComponentInfo, 0, len(rawComponents)) + for _, raw := range rawComponents { + components = append(components, ComponentInfo{ + Name: raw["name"], + FirmwareVersion: raw["firmware_version"], + Description: raw["description"], + }) + } + + return components, nil +} + +// GetModuleComponents calls Platform API to get module components +func GetModuleComponents() ([]ModuleComponentInfo, error) { + escaped := strings.ReplaceAll(common.ModuleComponentsPyScript, "'", `'\''`) + command := fmt.Sprintf("python3 -c '%s'", escaped) + + output, err := common.GetDataFromHostCommand(command) + if err != nil { + return nil, err + } + + var rawComponents []map[string]string + if err := json.Unmarshal([]byte(output), &rawComponents); err != nil { + return nil, err + } + + // Parse into Go structs + components := make([]ModuleComponentInfo, 0, len(rawComponents)) + for _, raw := range rawComponents { + components = append(components, ModuleComponentInfo{ + ModuleName: raw["module_name"], + Name: raw["name"], + FirmwareVersion: raw["firmware_version"], + Description: raw["description"], + }) + } + + return components, nil +} diff --git a/show_client/platform_cli.go b/show_client/platform_cli.go index 00307374..e181a968 100644 --- a/show_client/platform_cli.go +++ b/show_client/platform_cli.go @@ -93,6 +93,20 @@ type CurrentInfo struct { Timestamp string `json:"timestamp"` } +// ComponentInfo represents individual component firmware information +type ComponentInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` +} + +// FirmwareInfo represents the complete firmware status +type FirmwareInfo struct { + Chassis string `json:"chassis"` + Module string `json:"module"` + Components []ComponentInfo `json:"components"` +} + // SsdHealthInfo represents SSD health information. // Fields are conditionally populated based on verbose/vendor options, // matching Python ssdutil output: @@ -411,6 +425,67 @@ func getPlatformCurrent(args sdc.CmdArgs, options sdc.OptionMap) ([]byte, error) }) } +// getPlatformFirmware implements the "show platform firmware status" command +func getPlatformFirmware(args sdc.CmdArgs, options sdc.OptionMap) ([]byte, error) { + // Get all firmware data using helper + firmwareDataList, err := helpers.GetAllFirmwareData() + if err != nil { + log.V(1).Infof("Error getting firmware data: %v", err) + return json.Marshal(FirmwareInfo{ + Chassis: "N/A", + Module: "N/A", + Components: []ComponentInfo{}, + }) + } + + if len(firmwareDataList) == 0 { + return json.Marshal(FirmwareInfo{ + Chassis: "N/A", + Module: "N/A", + Components: []ComponentInfo{}, + }) + } + + // Extract chassis and module from first entry, build components list + chassisName := "N/A" + moduleName := "N/A" + components := make([]ComponentInfo, 0, len(firmwareDataList)) + + for i, data := range firmwareDataList { + // Use chassis name from first non-empty entry + if i == 0 || (chassisName == "N/A" && data.Chassis != "") { + if data.Chassis != "" { + chassisName = data.Chassis + } + } + + // For module, use first non-empty module name, or keep "N/A" if all are empty + if i == 0 || (moduleName == "N/A" && data.Module != "") { + if data.Module != "" { + moduleName = data.Module + } + } + + // Create component info + component := ComponentInfo{ + Name: data.Component, + Version: data.Version, + Description: data.Description, + } + + components = append(components, component) + } + + // Build final response structure + firmwareInfo := FirmwareInfo{ + Chassis: chassisName, + Module: moduleName, + Components: components, + } + + return json.Marshal(firmwareInfo) +} + // getPlatformSyseeprom implements "show platform syseeprom". // 1. Get platform name // 2. If platform matches kvm → return "does not support EEPROM" diff --git a/show_client/show_paths.go b/show_client/show_paths.go index 52414c55..a55b7773 100644 --- a/show_client/show_paths.go +++ b/show_client/show_paths.go @@ -1132,6 +1132,17 @@ func init() { nil, ) + // SHOW/platform/firmware/status + sdc.RegisterCliPath( + []string{"SHOW", "platform", "firmware", "status"}, + getPlatformFirmware, + "SHOW/platform/firmware/status[OPTIONS]: Show platform component firmware status", + 0, + 0, + nil, + showCmdOptionVerbose, + ) + //SHOW/boot sdc.RegisterCliPath( []string{"SHOW", "boot"},