Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
478 changes: 478 additions & 0 deletions gnmi_server/boot_cli_test.go

Large diffs are not rendered by default.

402 changes: 402 additions & 0 deletions gnmi_server/boot_helpers_test.go

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions show_client/boot_cli.go
Original file line number Diff line number Diff line change
@@ -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)
}
7 changes: 7 additions & 0 deletions show_client/common/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
67 changes: 67 additions & 0 deletions show_client/helpers/boot_helpers/aboot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package helpers

import (
"fmt"
"os"
"regexp"
"strings"

"github.com/sonic-net/sonic-gnmi/show_client/common"
)

type AbootBootloader struct{}

func (a *AbootBootloader) Name() string { return "aboot" }

func (a *AbootBootloader) GetCurrentImage() (string, error) {
// Use aboot-specific regex pattern
return getCurrentImageFromCmdline(`loop=/*(\S+)/`)
Comment thread
Deepak-Pandey marked this conversation as resolved.
}

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) {
configData, err := common.ReadConfToMap(AbootBootConfigPath)
if err != nil {
return "", err
}

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 {
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
}
49 changes: 49 additions & 0 deletions show_client/helpers/boot_helpers/boot_helper.go
Original file line number Diff line number Diff line change
@@ -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")
}
82 changes: 82 additions & 0 deletions show_client/helpers/boot_helpers/grub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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) {
Comment thread
Deepak-Pandey marked this conversation as resolved.
return getCurrentImageFromCmdline(`loop=(\S+)/fs\.squashfs`)
}

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
}
34 changes: 34 additions & 0 deletions show_client/helpers/boot_helpers/onie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 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 pattern not found in cmdline")
}

current := m[1]
result := strings.Replace(current, ImageDirPrefix, ImagePrefix, 1)

return result, nil
}
58 changes: 58 additions & 0 deletions show_client/helpers/boot_helpers/uboot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package helpers

import (
"fmt"
"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) {
return getCurrentImageFromCmdline(`loop=(\S+)/fs\.squashfs`)
}

func (u *UbootBootloader) GetInstalledImages() ([]string, error) {
var images []string

Comment thread
Deepak-Pandey marked this conversation as resolved.
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)
}
}
}

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
}
10 changes: 10 additions & 0 deletions show_client/show_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,16 @@ 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"},
Expand Down
Loading