From b1c81d09408201b1e8d5a64b53e6b18b4bdfaf2d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 25 Apr 2026 06:38:07 +0000 Subject: [PATCH 1/5] Add show boot command with CLI and tests --- gnmi_server/boot_cli_test.go | 478 ++++++++++++++++++ gnmi_server/boot_helpers_test.go | 310 ++++++++++++ show_client/boot_cli.go | 50 ++ show_client/helpers/boot_helpers/aboot.go | 89 ++++ .../helpers/boot_helpers/boot_helper.go | 49 ++ show_client/helpers/boot_helpers/grub.go | 86 ++++ show_client/helpers/boot_helpers/onie.go | 29 ++ show_client/helpers/boot_helpers/uboot.go | 65 +++ show_client/show_paths.go | 8 + 9 files changed, 1164 insertions(+) create mode 100644 gnmi_server/boot_cli_test.go create mode 100644 gnmi_server/boot_helpers_test.go create mode 100644 show_client/boot_cli.go create mode 100644 show_client/helpers/boot_helpers/aboot.go create mode 100644 show_client/helpers/boot_helpers/boot_helper.go create mode 100644 show_client/helpers/boot_helpers/grub.go create mode 100644 show_client/helpers/boot_helpers/onie.go create mode 100644 show_client/helpers/boot_helpers/uboot.go diff --git a/gnmi_server/boot_cli_test.go b/gnmi_server/boot_cli_test.go new file mode 100644 index 00000000..515038a4 --- /dev/null +++ b/gnmi_server/boot_cli_test.go @@ -0,0 +1,478 @@ +package gnmi + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "testing" + "time" + + pb "github.com/openconfig/gnmi/proto/gnmi" + + "github.com/agiledragon/gomonkey/v2" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + + "github.com/sonic-net/sonic-gnmi/show_client/helpers/boot_helpers" +) + +// Mock bootloader for testing +type mockBootloader struct { + name string + currentImage string + nextImage string + installedImages []string + currentErr error + nextErr error + installedErr error +} + +func (m *mockBootloader) Name() string { + return m.name +} + +func (m *mockBootloader) GetCurrentImage() (string, error) { + return m.currentImage, m.currentErr +} + +func (m *mockBootloader) GetNextImage() (string, error) { + return m.nextImage, m.nextErr +} + +func (m *mockBootloader) GetInstalledImages() ([]string, error) { + return m.installedImages, m.installedErr +} + +func TestGetShowBoot(t *testing.T) { + s := createServer(t, ServerPort) + go runServer(t, s) + defer s.ForceStop() + defer ResetDataSetsAndMappings(t) + + 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() + + tests := []struct { + desc string + pathTarget string + textPbPath string + wantRetCode codes.Code + wantRespVal []byte + valTest bool + setupFunc func() *gomonkey.Patches + }{ + { + desc: "query SHOW boot success", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-20240101.01","next":"SONiC-20240201.01","available":["SONiC-20240101.01","SONiC-20240201.01","SONiC-20240301.01"]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-20240101.01", + nextImage: "SONiC-20240201.01", + installedImages: []string{"SONiC-20240101.01", "SONiC-20240201.01", "SONiC-20240301.01"}, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot with empty installed images", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-20240101.01","next":"SONiC-20240201.01","available":[]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "aboot", + currentImage: "SONiC-20240101.01", + nextImage: "SONiC-20240201.01", + installedImages: nil, // Should be converted to empty array + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot with uboot", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-uboot.01","next":"SONiC-uboot.02","available":["SONiC-uboot.01","SONiC-uboot.02"]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "uboot", + currentImage: "SONiC-uboot.01", + nextImage: "SONiC-uboot.02", + installedImages: []string{"SONiC-uboot.01", "SONiC-uboot.02"}, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot detect bootloader error", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.NotFound, + wantRespVal: nil, + valTest: false, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return nil, errors.New("no supported bootloader detected") + }) + return patches + }, + }, + { + desc: "query SHOW boot get current image error", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.NotFound, + wantRespVal: nil, + valTest: false, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentErr: errors.New("failed to get current image"), + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot get next image error", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.NotFound, + wantRespVal: nil, + valTest: false, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-20240101.01", + nextErr: errors.New("failed to get next image"), + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot get installed images error", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.NotFound, + wantRespVal: nil, + valTest: false, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-20240101.01", + nextImage: "SONiC-20240201.01", + installedErr: errors.New("failed to get installed images"), + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot with special characters", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-OS.2024-01-01","next":"SONiC-OS.2024-02-01","available":["SONiC-OS.2024-01-01","SONiC-OS.2024-02-01"]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-OS.2024-01-01", + nextImage: "SONiC-OS.2024-02-01", + installedImages: []string{"SONiC-OS.2024-01-01", "SONiC-OS.2024-02-01"}, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot with single image", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-single","next":"SONiC-single","available":["SONiC-single"]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-single", + nextImage: "SONiC-single", + installedImages: []string{"SONiC-single"}, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot with large image list", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-20240101.00","next":"SONiC-20240101.01","available":["SONiC-20240101.00","SONiC-20240101.01","SONiC-20240101.02","SONiC-20240101.03","SONiC-20240101.04"]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + // Create a list of 5 images for testing + imageList := make([]string, 5) + for i := 0; i < 5; i++ { + imageList[i] = fmt.Sprintf("SONiC-20240101.%02d", i) + } + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-20240101.00", + nextImage: "SONiC-20240101.01", + installedImages: imageList, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + var patches *gomonkey.Patches + if test.setupFunc != nil { + patches = test.setupFunc() + defer patches.Reset() + } + runTestGet(t, ctx, gClient, test.pathTarget, test.textPbPath, test.wantRetCode, test.wantRespVal, test.valTest) + }) + } +} + +// Test edge cases and bootloader-specific behaviors +func TestGetShowBootEdgeCases(t *testing.T) { + s := createServer(t, ServerPort) + go runServer(t, s) + defer s.ForceStop() + defer ResetDataSetsAndMappings(t) + + 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() + + tests := []struct { + desc string + pathTarget string + textPbPath string + wantRetCode codes.Code + wantRespVal []byte + valTest bool + setupFunc func() *gomonkey.Patches + }{ + { + desc: "query SHOW boot with empty strings", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"","next":"","available":[]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "test", + currentImage: "", + nextImage: "", + installedImages: []string{}, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + { + desc: "query SHOW boot with very long image names", + pathTarget: "SHOW", + textPbPath: ` + elem: + `, + wantRetCode: codes.OK, + wantRespVal: []byte(`{"current":"SONiC-very-long-image-name-with-multiple-components-2024.01.01.build.123456","next":"SONiC-very-long-image-name-with-multiple-components-2024.02.01.build.234567","available":["SONiC-very-long-image-name-with-multiple-components-2024.01.01.build.123456","SONiC-very-long-image-name-with-multiple-components-2024.02.01.build.234567"]}`), + valTest: true, + setupFunc: func() *gomonkey.Patches { + patches := gomonkey.NewPatches() + mockBL := &mockBootloader{ + name: "grub", + currentImage: "SONiC-very-long-image-name-with-multiple-components-2024.01.01.build.123456", + nextImage: "SONiC-very-long-image-name-with-multiple-components-2024.02.01.build.234567", + installedImages: []string{"SONiC-very-long-image-name-with-multiple-components-2024.01.01.build.123456", "SONiC-very-long-image-name-with-multiple-components-2024.02.01.build.234567"}, + } + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + return patches + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + var patches *gomonkey.Patches + if test.setupFunc != nil { + patches = test.setupFunc() + defer patches.Reset() + } + runTestGet(t, ctx, gClient, test.pathTarget, test.textPbPath, test.wantRetCode, test.wantRespVal, test.valTest) + }) + } +} + +// Test different bootloader types +func TestGetShowBootDifferentBootloaders(t *testing.T) { + s := createServer(t, ServerPort) + go runServer(t, s) + defer s.ForceStop() + defer ResetDataSetsAndMappings(t) + + 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() + + bootloaderTests := []struct { + name string + bootloaderName string + currentImage string + nextImage string + installedImages []string + expectedJSON string + }{ + { + name: "Aboot", + bootloaderName: "aboot", + currentImage: "SONiC-aboot.01", + nextImage: "SONiC-aboot.02", + installedImages: []string{"SONiC-aboot.01", "SONiC-aboot.02"}, + expectedJSON: `{"current":"SONiC-aboot.01","next":"SONiC-aboot.02","available":["SONiC-aboot.01","SONiC-aboot.02"]}`, + }, + { + name: "GRUB", + bootloaderName: "grub", + currentImage: "SONiC-grub.01", + nextImage: "SONiC-grub.02", + installedImages: []string{"SONiC-grub.01", "SONiC-grub.02"}, + expectedJSON: `{"current":"SONiC-grub.01","next":"SONiC-grub.02","available":["SONiC-grub.01","SONiC-grub.02"]}`, + }, + { + name: "U-Boot", + bootloaderName: "uboot", + currentImage: "SONiC-uboot.01", + nextImage: "SONiC-uboot.02", + installedImages: []string{"SONiC-uboot.01", "SONiC-uboot.02"}, + expectedJSON: `{"current":"SONiC-uboot.01","next":"SONiC-uboot.02","available":["SONiC-uboot.01","SONiC-uboot.02"]}`, + }, + } + + for _, bt := range bootloaderTests { + t.Run("SHOW boot with "+bt.name, func(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + mockBL := &mockBootloader{ + name: bt.bootloaderName, + currentImage: bt.currentImage, + nextImage: bt.nextImage, + installedImages: bt.installedImages, + } + + patches.ApplyFunc(helpers.DetectBootloader, func() (helpers.Bootloader, error) { + return mockBL, nil + }) + + runTestGet(t, ctx, gClient, "SHOW", `elem: `, codes.OK, []byte(bt.expectedJSON), true) + }) + } +} diff --git a/gnmi_server/boot_helpers_test.go b/gnmi_server/boot_helpers_test.go new file mode 100644 index 00000000..99d49f49 --- /dev/null +++ b/gnmi_server/boot_helpers_test.go @@ -0,0 +1,310 @@ +package gnmi + +import ( + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/agiledragon/gomonkey/v2" + "github.com/sonic-net/sonic-gnmi/show_client/common" + helpers "github.com/sonic-net/sonic-gnmi/show_client/helpers/boot_helpers" +) + +func TestBootHelperDetectBootloader(t *testing.T) { + tests := []struct { + name string + mockCmdline string + mockCmdlineErr error + grubCfgExists bool + expectedType string + expectError bool + }{ + { + name: "Aboot detection", + mockCmdline: "console=ttyS0,9600 Aboot=Aboot-veos-8.0.0", + grubCfgExists: false, + expectedType: "aboot", + expectError: false, + }, + { + name: "GRUB detection", + mockCmdline: "BOOT_IMAGE=/vmlinuz-4.19.0-12-amd64", + grubCfgExists: true, + expectedType: "grub", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + // Mock file operations for proc/cmdline + patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { + if name == "/proc/cmdline" { + if tt.mockCmdlineErr != nil { + return nil, tt.mockCmdlineErr + } + return []byte(tt.mockCmdline), nil + } + return nil, os.ErrNotExist + }) + + // Mock os.Stat for GRUB detection + patches.ApplyFunc(os.Stat, func(name string) (os.FileInfo, error) { + if strings.Contains(name, "grub.cfg") && tt.grubCfgExists { + return &mockFileInfo{name: "grub.cfg", isDir: false}, nil + } + return nil, os.ErrNotExist + }) + + bl, err := helpers.DetectBootloader() + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if bl.Name() != tt.expectedType { + t.Errorf("Expected bootloader %s, got %s", tt.expectedType, bl.Name()) + } + }) + } +} + +// Mock FileInfo for testing +type mockFileInfo struct { + name string + isDir bool +} + +func (m *mockFileInfo) Name() string { return m.name } +func (m *mockFileInfo) Size() int64 { return 0 } +func (m *mockFileInfo) Mode() os.FileMode { return 0 } +func (m *mockFileInfo) ModTime() time.Time { return time.Time{} } +func (m *mockFileInfo) IsDir() bool { return m.isDir } +func (m *mockFileInfo) Sys() interface{} { return nil } + +func TestBootHelperAbootBootloader(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + // Mock file operations for simplified Aboot implementation + patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { + if name == "/proc/cmdline" { + return []byte("loop=image-20240101.01/fs.squashfs"), nil + } + if name == "/host/boot-config" { + return []byte("# Boot configuration\nSWI=flash:/image-20240201.01/sonic.swi\n"), nil + } + return nil, os.ErrNotExist + }) + + patches.ApplyFunc(os.ReadDir, func(name string) ([]os.DirEntry, error) { + if name == "/host" { + return []os.DirEntry{ + &mockDirEntry{name: "image-20240101.01", isDir: true}, + &mockDirEntry{name: "image-20240201.01", isDir: true}, + }, nil + } + return nil, os.ErrNotExist + }) + + bl := &helpers.AbootBootloader{} + + // Test Name() + if bl.Name() != "aboot" { + t.Errorf("Expected name 'aboot', got %q", bl.Name()) + } + + // Test GetCurrentImage() + current, err := bl.GetCurrentImage() + if err != nil { + t.Fatalf("GetCurrentImage error: %v", err) + } + expected := "SONiC-OS-20240101.01" + if current != expected { + t.Errorf("Expected current image %q, got %q", expected, current) + } + + // Test GetInstalledImages() + images, err := bl.GetInstalledImages() + if err != nil { + t.Fatalf("GetInstalledImages error: %v", err) + } + if len(images) != 2 { + t.Errorf("Expected 2 images, got %d", len(images)) + } + + // Test GetNextImage() + next, err := bl.GetNextImage() + if err != nil { + t.Fatalf("GetNextImage error: %v", err) + } + expected = "SONiC-OS-20240201.01" + if next != expected { + t.Errorf("Expected next image %q, got %q", expected, next) + } +} + +type mockDirEntry struct { + name string + isDir bool +} + +func (m *mockDirEntry) Name() string { return m.name } +func (m *mockDirEntry) IsDir() bool { return m.isDir } +func (m *mockDirEntry) Type() os.FileMode { return 0 } +func (m *mockDirEntry) Info() (os.FileInfo, error) { return nil, nil } + +func TestBootHelperGrubBootloader(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + // Mock file operations for simplified GRUB implementation + patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { + if name == "/proc/cmdline" { + return []byte("loop=image-grub-test.01/fs.squashfs"), nil + } + if strings.Contains(name, "grub.cfg") { + return []byte(` +menuentry 'SONiC-OS-20240101.01' { + linux /vmlinuz +} +menuentry 'SONiC-OS-20240201.01' { + linux /vmlinuz +} +`), nil + } + return nil, os.ErrNotExist + }) + + // Mock common.GetDataFromHostCommand for grub-editenv + patches.ApplyFunc(common.GetDataFromHostCommand, func(command string) (string, error) { + if strings.Contains(command, "grub-editenv") && strings.Contains(command, "list") { + return "next_entry=1\nsaved_entry=0\n", nil + } + return "", os.ErrNotExist + }) + + bl := &helpers.GrubBootloader{} + + // Test Name() + if bl.Name() != "grub" { + t.Errorf("Expected name 'grub', got %q", bl.Name()) + } + + // Test GetCurrentImage() + current, err := bl.GetCurrentImage() + if err != nil { + t.Fatalf("GetCurrentImage error: %v", err) + } + expected := "SONiC-OS-grub-test.01" + if current != expected { + t.Errorf("Expected current image %q, got %q", expected, current) + } + + // Test GetInstalledImages() + images, err := bl.GetInstalledImages() + if err != nil { + t.Fatalf("GetInstalledImages error: %v", err) + } + if len(images) != 2 { + t.Errorf("Expected 2 images, got %d", len(images)) + } + + // Test GetNextImage() + next, err := bl.GetNextImage() + if err != nil { + t.Fatalf("GetNextImage error: %v", err) + } + expected = "SONiC-OS-20240201.01" + if next != expected { + t.Errorf("Expected next image %q, got %q", expected, next) + } +} + +func TestBootHelperUbootBootloader(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + // Mock file operations for /proc/cmdline + patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { + if name == "/proc/cmdline" { + return []byte("loop=image-uboot-test.01/fs.squashfs"), nil + } + return nil, os.ErrNotExist + }) + + // Mock common.GetDataFromHostCommand for fw_printenv + patches.ApplyFunc(common.GetDataFromHostCommand, func(command string) (string, error) { + if strings.Contains(command, "fw_printenv") { + if strings.Contains(command, "sonic_version_1") { + return "SONiC-OS-20240101.01", nil + } + if strings.Contains(command, "sonic_version_2") { + return "SONiC-OS-20240201.01", nil + } + if strings.Contains(command, "boot_next") { + return "sonic_image_2", nil + } + } + return "", os.ErrNotExist + }) + + bl := &helpers.UbootBootloader{} + + // Test Name() + if bl.Name() != "uboot" { + t.Errorf("Expected name 'uboot', got %q", bl.Name()) + } + + // Test GetCurrentImage() + current, err := bl.GetCurrentImage() + if err != nil { + t.Fatalf("GetCurrentImage error: %v", err) + } + expected := "SONiC-OS-uboot-test.01" + if current != expected { + t.Errorf("Expected current image %q, got %q", expected, current) + } + + // Test GetInstalledImages() + images, err := bl.GetInstalledImages() + if err != nil { + t.Fatalf("GetInstalledImages error: %v", err) + } + if len(images) != 2 { + t.Errorf("Expected 2 images, got %d", len(images)) + } + + // Test GetNextImage() + next, err := bl.GetNextImage() + if err != nil { + t.Fatalf("GetNextImage error: %v", err) + } + expected = "SONiC-OS-20240201.01" + if next != expected { + t.Errorf("Expected next image %q, got %q", expected, next) + } +} + +func TestBootHelperIntegration(t *testing.T) { + // Test that DetectBootloader returns valid interface in current environment + if runtime.GOARCH == "amd64" { + _, err := helpers.DetectBootloader() + // We expect an error in test environment, which is fine + if err != nil { + t.Logf("Expected error in test environment: %v", err) + } + } +} diff --git a/show_client/boot_cli.go b/show_client/boot_cli.go new file mode 100644 index 00000000..166a9be1 --- /dev/null +++ b/show_client/boot_cli.go @@ -0,0 +1,50 @@ +package show_client + +import ( + "encoding/json" + + log "github.com/golang/glog" + "github.com/sonic-net/sonic-gnmi/show_client/helpers/boot_helpers" + sdc "github.com/sonic-net/sonic-gnmi/sonic_data_client" +) + +type bootResponse struct { + Current string `json:"current"` + Next string `json:"next"` + Available []string `json:"available"` +} + +func getBoot(_ sdc.CmdArgs, _ sdc.OptionMap) ([]byte, error) { + bl, err := helpers.DetectBootloader() + if err != nil { + log.Errorf("Failed to detect bootloader: %v", err) + return nil, err + } + + current, err := bl.GetCurrentImage() + if err != nil { + return nil, err + } + + next, err := bl.GetNextImage() + if err != nil { + return nil, err + } + + images, err := bl.GetInstalledImages() + if err != nil { + return nil, err + } + + if images == nil { + images = []string{} + } + + resp := bootResponse{ + Current: current, + Next: next, + Available: images, + } + + return json.Marshal(resp) +} diff --git a/show_client/helpers/boot_helpers/aboot.go b/show_client/helpers/boot_helpers/aboot.go new file mode 100644 index 00000000..c7ec657c --- /dev/null +++ b/show_client/helpers/boot_helpers/aboot.go @@ -0,0 +1,89 @@ +package helpers + +import ( + "fmt" + "os" + "regexp" + "strings" +) + +type AbootBootloader struct{} + +func (a *AbootBootloader) Name() string { return "aboot" } + +func (a *AbootBootloader) GetCurrentImage() (string, error) { + cmdline, err := readProcCmdline() + if err != nil { + return "", err + } + return currentImageFromCmdline(cmdline) +} + +func (a *AbootBootloader) GetInstalledImages() ([]string, error) { + files, err := os.ReadDir(HostPath) + if err != nil { + return nil, err + } + + var images []string + for _, file := range files { + if file.IsDir() && strings.HasPrefix(file.Name(), ImageDirPrefix) { + image := strings.Replace(file.Name(), ImageDirPrefix, ImagePrefix, 1) + images = append(images, image) + } + } + + return images, nil +} + +func (a *AbootBootloader) GetNextImage() (string, error) { + // Read boot config + config, err := a.bootConfigRead() + if err != nil { + return "", err + } + + swi := config["SWI"] + if swi == "" { + return "", fmt.Errorf("SWI not found in boot config") + } + + re := regexp.MustCompile(`flash:/*(\S+)/`) + m := re.FindStringSubmatch(swi) + if len(m) >= 2 { + return strings.Replace(m[1], ImageDirPrefix, ImagePrefix, 1), nil + } + + // Fallback: swi.split(':', 1)[-1] + parts := strings.SplitN(swi, ":", 2) + if len(parts) == 2 { + return parts[1], nil + } + + return swi, nil +} + +func (a *AbootBootloader) bootConfigRead() (map[string]string, error) { + config := make(map[string]string) + + data, err := os.ReadFile(AbootBootConfigPath) + if err != nil { + return nil, err + } + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + if parts := strings.SplitN(line, "=", 2); len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + config[key] = value + } + } + + return config, nil +} diff --git a/show_client/helpers/boot_helpers/boot_helper.go b/show_client/helpers/boot_helpers/boot_helper.go new file mode 100644 index 00000000..978a3708 --- /dev/null +++ b/show_client/helpers/boot_helpers/boot_helper.go @@ -0,0 +1,49 @@ +package helpers + +import ( + "fmt" + "os" + "runtime" + "strings" +) + +// Shared constants +const ( + HostPath = "/host" + ImageDirPrefix = "image-" + ImagePrefix = "SONiC-OS-" + + // Aboot + AbootBootConfigPath = "/host/boot-config" + + // GRUB + GrubCfgPath = "/host/grub/grub.cfg" + GrubEnvPath = "/host/grub/grubenv" +) + +type Bootloader interface { + Name() string + GetCurrentImage() (string, error) + GetNextImage() (string, error) + GetInstalledImages() ([]string, error) +} + +func DetectBootloader() (Bootloader, error) { + // 1. Check for Aboot + cmdline, err := readProcCmdline() + if err == nil && strings.Contains(cmdline, "Aboot=") { + return &AbootBootloader{}, nil + } + + // 2. Check for GRUB + if _, err := os.Stat(GrubCfgPath); err == nil { + return &GrubBootloader{}, nil + } + + // 3. Check for U-Boot + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + return &UbootBootloader{}, nil + } + + return nil, fmt.Errorf("No supported bootloader detected") +} diff --git a/show_client/helpers/boot_helpers/grub.go b/show_client/helpers/boot_helpers/grub.go new file mode 100644 index 00000000..de9e1272 --- /dev/null +++ b/show_client/helpers/boot_helpers/grub.go @@ -0,0 +1,86 @@ +package helpers + +import ( + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "github.com/sonic-net/sonic-gnmi/show_client/common" +) + +type GrubBootloader struct{} + +func (g *GrubBootloader) Name() string { return "grub" } + +func (g *GrubBootloader) GetCurrentImage() (string, error) { + cmdline, err := readProcCmdline() + if err != nil { + return "", err + } + return currentImageFromCmdline(cmdline) +} + +func (g *GrubBootloader) GetInstalledImages() ([]string, error) { + data, err := os.ReadFile(GrubCfgPath) + if err != nil { + return nil, err + } + + var images []string + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + if strings.HasPrefix(line, "menuentry") { + parts := strings.Fields(line) + if len(parts) >= 2 { + image := strings.Trim(parts[1], "'\"") + if strings.Contains(image, ImagePrefix) { + images = append(images, image) + } + } + } + } + + return images, nil +} + +func (g *GrubBootloader) GetNextImage() (string, error) { + images, err := g.GetInstalledImages() + if err != nil { + return "", err + } + + if len(images) == 0 { + return "", fmt.Errorf("no installed images found") + } + + command := fmt.Sprintf("/usr/bin/grub-editenv %s list", GrubEnvPath) + output, err := common.GetDataFromHostCommand(command) + if err != nil { + return images[0], nil + } + + nextImageIndex := 0 + + re := regexp.MustCompile(`next_entry=(\d+)`) + if m := re.FindStringSubmatch(output); len(m) >= 2 { + if idx, err := strconv.Atoi(m[1]); err == nil { + nextImageIndex = idx + } + } else { + re = regexp.MustCompile(`saved_entry=(\d+)`) + if m := re.FindStringSubmatch(output); len(m) >= 2 { + if idx, err := strconv.Atoi(m[1]); err == nil { + nextImageIndex = idx + } + } + } + + if nextImageIndex < 0 || nextImageIndex >= len(images) { + nextImageIndex = 0 + } + + return images[nextImageIndex], nil +} diff --git a/show_client/helpers/boot_helpers/onie.go b/show_client/helpers/boot_helpers/onie.go new file mode 100644 index 00000000..04582454 --- /dev/null +++ b/show_client/helpers/boot_helpers/onie.go @@ -0,0 +1,29 @@ +package helpers + +import ( + "fmt" + "os" + "regexp" + "strings" +) + +func readProcCmdline() (string, error) { + data, err := os.ReadFile("/proc/cmdline") + if err != nil { + return "", err + } + return strings.TrimSpace(string(data)), nil +} + +func currentImageFromCmdline(cmdline string) (string, error) { + re := regexp.MustCompile(`loop=(\S+)/fs\.squashfs`) + m := re.FindStringSubmatch(cmdline) + if len(m) < 2 { + return "", fmt.Errorf("loop mount with fs.squashfs not found in cmdline") + } + + current := m[1] + result := strings.Replace(current, ImageDirPrefix, ImagePrefix, 1) + + return result, nil +} diff --git a/show_client/helpers/boot_helpers/uboot.go b/show_client/helpers/boot_helpers/uboot.go new file mode 100644 index 00000000..b50bc669 --- /dev/null +++ b/show_client/helpers/boot_helpers/uboot.go @@ -0,0 +1,65 @@ +package helpers + +import ( + "strings" + + "github.com/sonic-net/sonic-gnmi/show_client/common" +) + +type UbootBootloader struct{} + +func (u *UbootBootloader) Name() string { return "uboot" } + +func (u *UbootBootloader) GetCurrentImage() (string, error) { + cmdline, err := readProcCmdline() + if err != nil { + return "", err + } + return currentImageFromCmdline(cmdline) +} + +func (u *UbootBootloader) GetInstalledImages() ([]string, error) { + var images []string + + if output, err := common.GetDataFromHostCommand("/usr/bin/fw_printenv -n sonic_version_1"); err == nil { + image := strings.TrimSpace(output) + if strings.Contains(image, ImagePrefix) { + images = append(images, image) + } + } + + if output, err := common.GetDataFromHostCommand("/usr/bin/fw_printenv -n sonic_version_2"); err == nil { + image := strings.TrimSpace(output) + if strings.Contains(image, ImagePrefix) { + images = append(images, image) + } + } + + return images, nil +} + +func (u *UbootBootloader) GetNextImage() (string, error) { + images, err := u.GetInstalledImages() + if err != nil { + return "", err + } + + output, err := common.GetDataFromHostCommand("/usr/bin/fw_printenv -n boot_next") + if err != nil { + if len(images) > 0 { + return images[0], nil + } + return "", err + } + + bootNext := strings.TrimSpace(output) + if strings.Contains(bootNext, "sonic_image_2") && len(images) == 2 { + return images[1], nil + } + + if len(images) > 0 { + return images[0], nil + } + + return "", nil +} diff --git a/show_client/show_paths.go b/show_client/show_paths.go index 75827ba9..d64e69a7 100644 --- a/show_client/show_paths.go +++ b/show_client/show_paths.go @@ -1101,4 +1101,12 @@ func init() { nil, sdc.UnimplementedOption(showCmdOptionNamespace), ) + sdc.RegisterCliPath( + []string{"SHOW", "boot"}, + getBoot, + "SHOW/boot[OPTIONS]: Show boot configuration", + 0, + 0, + nil, + ) } From 2f0f05a14937d36699469bd21625522dd5eed229 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 29 Apr 2026 08:47:59 +0000 Subject: [PATCH 2/5] Updated with review comments --- gnmi_server/boot_helpers_test.go | 98 ++++++++++++++++++++++- show_client/common/file.go | 7 ++ show_client/helpers/boot_helpers/aboot.go | 46 +++-------- show_client/helpers/boot_helpers/grub.go | 6 +- show_client/helpers/boot_helpers/onie.go | 11 ++- show_client/helpers/boot_helpers/uboot.go | 25 +++--- 6 files changed, 132 insertions(+), 61 deletions(-) diff --git a/gnmi_server/boot_helpers_test.go b/gnmi_server/boot_helpers_test.go index 99d49f49..bad5fafb 100644 --- a/gnmi_server/boot_helpers_test.go +++ b/gnmi_server/boot_helpers_test.go @@ -10,6 +10,7 @@ import ( "github.com/agiledragon/gomonkey/v2" "github.com/sonic-net/sonic-gnmi/show_client/common" helpers "github.com/sonic-net/sonic-gnmi/show_client/helpers/boot_helpers" + sdc "github.com/sonic-net/sonic-gnmi/sonic_data_client" ) func TestBootHelperDetectBootloader(t *testing.T) { @@ -98,17 +99,26 @@ func TestBootHelperAbootBootloader(t *testing.T) { patches := gomonkey.NewPatches() defer patches.Reset() + // Store original and restore after test + origImplIoutilReadFile := sdc.ImplIoutilReadFile + defer func() { sdc.ImplIoutilReadFile = origImplIoutilReadFile }() + // Mock file operations for simplified Aboot implementation patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { if name == "/proc/cmdline" { return []byte("loop=image-20240101.01/fs.squashfs"), nil } - if name == "/host/boot-config" { - return []byte("# Boot configuration\nSWI=flash:/image-20240201.01/sonic.swi\n"), nil - } return nil, os.ErrNotExist }) + // Mock sdc.ImplIoutilReadFile for boot-config (used by common.ReadConfToMap) + sdc.ImplIoutilReadFile = func(filePath string) ([]byte, error) { + if filePath == "/host/boot-config" { + return []byte("# Boot configuration\nSWI=flash:/image-20240201.01/sonic.swi\n"), nil + } + return origImplIoutilReadFile(filePath) + } + patches.ApplyFunc(os.ReadDir, func(name string) ([]os.DirEntry, error) { if name == "/host" { return []os.DirEntry{ @@ -308,3 +318,85 @@ func TestBootHelperIntegration(t *testing.T) { } } } + +func TestAbootCurrentImageFromCmdlineRegexFix(t *testing.T) { + tests := []struct { + name string + cmdline string + expected string + shouldError bool + description string + }{ + { + name: "Aboot flexible pattern without fs.squashfs", + cmdline: "console=ttyS0,9600 Aboot=Aboot-veos-8.0.0 loop=/image-20240101.01/ quiet", + expected: "SONiC-OS-20240101.01", + shouldError: false, + description: "Should work with aboot's flexible regex pattern (matches Python behavior)", + }, + { + name: "Aboot flexible pattern with fs.squashfs", + cmdline: "console=ttyS0,9600 Aboot=Aboot-veos-8.0.0 loop=image-20240201.02/fs.squashfs quiet", + expected: "SONiC-OS-20240201.02", + shouldError: false, + description: "Should also work with the standard onie pattern", + }, + { + name: "Aboot with multiple slashes", + cmdline: "console=ttyS0,9600 loop=//image-test.03/ quiet", + expected: "SONiC-OS-test.03", + shouldError: false, + description: "Should handle multiple slashes in loop parameter", + }, + { + name: "No loop parameter", + cmdline: "console=ttyS0,9600 quiet", + expected: "", + shouldError: true, + description: "Should fail when no loop parameter is present", + }, + { + name: "Invalid loop parameter format", + cmdline: "console=ttyS0,9600 loop=invalid-format quiet", + expected: "", + shouldError: true, + description: "Should fail when loop parameter doesn't match pattern", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + // Create aboot bootloader instance + bl := &helpers.AbootBootloader{} + + // Mock readProcCmdline to return our test cmdline + patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) { + if name == "/proc/cmdline" { + return []byte(tt.cmdline), nil + } + return nil, os.ErrNotExist + }) + + result, err := bl.GetCurrentImage() + + if tt.shouldError { + if err == nil { + t.Errorf("Expected error but got none. Test: %s", tt.description) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v. Test: %s", err, tt.description) + return + } + + if result != tt.expected { + t.Errorf("Expected %q, got %q. Test: %s", tt.expected, result, tt.description) + } + }) + } +} diff --git a/show_client/common/file.go b/show_client/common/file.go index b8c0efe7..1834c3a3 100644 --- a/show_client/common/file.go +++ b/show_client/common/file.go @@ -47,6 +47,13 @@ func ReadConfToMap(filePath string) (map[string]interface{}, error) { content := string(dataBytes) lines := strings.Split(content, "\n") for _, line := range lines { + line = strings.TrimSpace(line) + + // Skip empty lines and comment lines (starting with #) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.Contains(line, "=") { parts := strings.SplitN(line, "=", 2) key := strings.TrimSpace(parts[0]) diff --git a/show_client/helpers/boot_helpers/aboot.go b/show_client/helpers/boot_helpers/aboot.go index c7ec657c..da8b8df9 100644 --- a/show_client/helpers/boot_helpers/aboot.go +++ b/show_client/helpers/boot_helpers/aboot.go @@ -5,6 +5,8 @@ import ( "os" "regexp" "strings" + + "github.com/sonic-net/sonic-gnmi/show_client/common" ) type AbootBootloader struct{} @@ -12,11 +14,8 @@ type AbootBootloader struct{} func (a *AbootBootloader) Name() string { return "aboot" } func (a *AbootBootloader) GetCurrentImage() (string, error) { - cmdline, err := readProcCmdline() - if err != nil { - return "", err - } - return currentImageFromCmdline(cmdline) + // Use aboot-specific regex pattern + return getCurrentImageFromCmdline(`loop=/*(\S+)/`) } func (a *AbootBootloader) GetInstalledImages() ([]string, error) { @@ -37,17 +36,21 @@ func (a *AbootBootloader) GetInstalledImages() ([]string, error) { } func (a *AbootBootloader) GetNextImage() (string, error) { - // Read boot config - config, err := a.bootConfigRead() + configData, err := common.ReadConfToMap(AbootBootConfigPath) if err != nil { return "", err } - swi := config["SWI"] - if swi == "" { + swiInterface, exists := configData["SWI"] + if !exists { return "", fmt.Errorf("SWI not found in boot config") } + swi, ok := swiInterface.(string) + if !ok { + return "", fmt.Errorf("SWI value is not a string") + } + re := regexp.MustCompile(`flash:/*(\S+)/`) m := re.FindStringSubmatch(swi) if len(m) >= 2 { @@ -62,28 +65,3 @@ func (a *AbootBootloader) GetNextImage() (string, error) { return swi, nil } - -func (a *AbootBootloader) bootConfigRead() (map[string]string, error) { - config := make(map[string]string) - - data, err := os.ReadFile(AbootBootConfigPath) - if err != nil { - return nil, err - } - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - if parts := strings.SplitN(line, "=", 2); len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - config[key] = value - } - } - - return config, nil -} diff --git a/show_client/helpers/boot_helpers/grub.go b/show_client/helpers/boot_helpers/grub.go index de9e1272..a61c5e9f 100644 --- a/show_client/helpers/boot_helpers/grub.go +++ b/show_client/helpers/boot_helpers/grub.go @@ -15,11 +15,7 @@ type GrubBootloader struct{} func (g *GrubBootloader) Name() string { return "grub" } func (g *GrubBootloader) GetCurrentImage() (string, error) { - cmdline, err := readProcCmdline() - if err != nil { - return "", err - } - return currentImageFromCmdline(cmdline) + return getCurrentImageFromCmdline(`loop=(\S+)/fs\.squashfs`) } func (g *GrubBootloader) GetInstalledImages() ([]string, error) { diff --git a/show_client/helpers/boot_helpers/onie.go b/show_client/helpers/boot_helpers/onie.go index 04582454..51e614a2 100644 --- a/show_client/helpers/boot_helpers/onie.go +++ b/show_client/helpers/boot_helpers/onie.go @@ -15,11 +15,16 @@ func readProcCmdline() (string, error) { return strings.TrimSpace(string(data)), nil } -func currentImageFromCmdline(cmdline string) (string, error) { - re := regexp.MustCompile(`loop=(\S+)/fs\.squashfs`) +func getCurrentImageFromCmdline(regexPattern string) (string, error) { + cmdline, err := readProcCmdline() + if err != nil { + return "", err + } + + re := regexp.MustCompile(regexPattern) m := re.FindStringSubmatch(cmdline) if len(m) < 2 { - return "", fmt.Errorf("loop mount with fs.squashfs not found in cmdline") + return "", fmt.Errorf("loop mount pattern not found in cmdline") } current := m[1] diff --git a/show_client/helpers/boot_helpers/uboot.go b/show_client/helpers/boot_helpers/uboot.go index b50bc669..a8964f62 100644 --- a/show_client/helpers/boot_helpers/uboot.go +++ b/show_client/helpers/boot_helpers/uboot.go @@ -1,6 +1,7 @@ package helpers import ( + "fmt" "strings" "github.com/sonic-net/sonic-gnmi/show_client/common" @@ -11,27 +12,19 @@ type UbootBootloader struct{} func (u *UbootBootloader) Name() string { return "uboot" } func (u *UbootBootloader) GetCurrentImage() (string, error) { - cmdline, err := readProcCmdline() - if err != nil { - return "", err - } - return currentImageFromCmdline(cmdline) + return getCurrentImageFromCmdline(`loop=(\S+)/fs\.squashfs`) } func (u *UbootBootloader) GetInstalledImages() ([]string, error) { var images []string - if output, err := common.GetDataFromHostCommand("/usr/bin/fw_printenv -n sonic_version_1"); err == nil { - image := strings.TrimSpace(output) - if strings.Contains(image, ImagePrefix) { - images = append(images, image) - } - } - - if output, err := common.GetDataFromHostCommand("/usr/bin/fw_printenv -n sonic_version_2"); err == nil { - image := strings.TrimSpace(output) - if strings.Contains(image, ImagePrefix) { - images = append(images, image) + for i := 1; i <= 2; i++ { + cmd := fmt.Sprintf("/usr/bin/fw_printenv -n sonic_version_%d", i) + if output, err := common.GetDataFromHostCommand(cmd); err == nil { + image := strings.TrimSpace(output) + if strings.Contains(image, ImagePrefix) { + images = append(images, image) + } } } From 14c5933373fe8df03af424e39e62ab4e0a013800 Mon Sep 17 00:00:00 2001 From: Deepak-Pandey Date: Mon, 4 May 2026 14:10:54 +0530 Subject: [PATCH 3/5] Refactor CLI path registration for show commands --- show_client/show_paths.go | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/show_client/show_paths.go b/show_client/show_paths.go index 2a3dc1a6..ff219bb8 100644 --- a/show_client/show_paths.go +++ b/show_client/show_paths.go @@ -1132,35 +1132,35 @@ func init() { nil, ) - //SHOW/boot - sdc.RegisterCliPath( - []string{"SHOW", "boot"}, - getBoot, - "SHOW/boot[OPTIONS]: Show boot configuration", - 0, - 0, - nil, - ) - - // SHOW/platform/ssdhealth - sdc.RegisterCliPath( - []string{"SHOW", "platform", "ssdhealth"}, - getPlatformSsdhealth, - "SHOW/platform/ssdhealth/{DEVICE}[OPTIONS]: Show platform SSD health", - 0, - 1, - nil, - showCmdOptionVerbose, - showCmdOptionVendor, - ) - - //SHOW/management-interface/address - sdc.RegisterCliPath( - []string{"SHOW", "management-interface", "address"}, - getManagementInterfaceAddress, - "SHOW/management-interface/address[OPTIONS]: Show management interface parameters", - 0, - 0, - nil, - ) + //SHOW/boot + sdc.RegisterCliPath( + []string{"SHOW", "boot"}, + getBoot, + "SHOW/boot[OPTIONS]: Show boot configuration", + 0, + 0, + nil, + ) + + // SHOW/platform/ssdhealth + sdc.RegisterCliPath( + []string{"SHOW", "platform", "ssdhealth"}, + getPlatformSsdhealth, + "SHOW/platform/ssdhealth/{DEVICE}[OPTIONS]: Show platform SSD health", + 0, + 1, + nil, + showCmdOptionVerbose, + showCmdOptionVendor, + ) + + //SHOW/management-interface/address + sdc.RegisterCliPath( + []string{"SHOW", "management-interface", "address"}, + getManagementInterfaceAddress, + "SHOW/management-interface/address[OPTIONS]: Show management interface parameters", + 0, + 0, + nil, + ) } From 0a0290470f5474c22f11931f20132f9dfe2fb2ad Mon Sep 17 00:00:00 2001 From: Deepak-Pandey Date: Mon, 4 May 2026 14:12:16 +0530 Subject: [PATCH 4/5] Remove unnecessary blank line in show_paths.go --- show_client/show_paths.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/show_client/show_paths.go b/show_client/show_paths.go index ff219bb8..c05ef8a3 100644 --- a/show_client/show_paths.go +++ b/show_client/show_paths.go @@ -1153,7 +1153,7 @@ func init() { showCmdOptionVerbose, showCmdOptionVendor, ) - + //SHOW/management-interface/address sdc.RegisterCliPath( []string{"SHOW", "management-interface", "address"}, From ba674b1fd12866803456504ed7801ee91928e830 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 4 May 2026 08:51:38 +0000 Subject: [PATCH 5/5] merge conflicts fix --- show_client/show_paths.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/show_client/show_paths.go b/show_client/show_paths.go index c05ef8a3..dbc4ba60 100644 --- a/show_client/show_paths.go +++ b/show_client/show_paths.go @@ -1141,7 +1141,7 @@ func init() { 0, nil, ) - + // SHOW/platform/ssdhealth sdc.RegisterCliPath( []string{"SHOW", "platform", "ssdhealth"},