Files
monok8s/clitools/pkg/node/uboot.go

259 lines
5.9 KiB
Go

package node
import (
"bytes"
"context"
"fmt"
"k8s.io/klog/v2"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
func ConfigureUBootCommands(ctx context.Context, n *NodeContext) error {
const (
procMTDPath = "/proc/mtd"
fwEnvCfgPath = "/etc/fw_env.config"
targetName = "uboot-env"
)
_ = n
type mtdInfo struct {
dev string
sizeHex string
eraseHex string
name string
partSize uint64
eraseSize uint64
}
parseHex := func(s string) (uint64, error) {
var v uint64
_, err := fmt.Sscanf(s, "%x", &v)
if err != nil {
return 0, fmt.Errorf("parse hex %q: %w", s, err)
}
return v, nil
}
raw, err := os.ReadFile(procMTDPath)
if err != nil {
return fmt.Errorf("read %s: %w", procMTDPath, err)
}
lineRE := regexp.MustCompile(`^(mtd\d+):\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+"([^"]+)"$`)
var envMTD *mtdInfo
for _, line := range strings.Split(string(raw), "\n") {
klog.V(4).Info(line)
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "dev:") {
continue
}
m := lineRE.FindStringSubmatch(line)
if m == nil {
continue
}
if m[4] != targetName {
continue
}
partSize, err := parseHex(m[2])
if err != nil {
return fmt.Errorf("parse partition size for %s: %w", m[1], err)
}
eraseSize, err := parseHex(m[3])
if err != nil {
return fmt.Errorf("parse erase size for %s: %w", m[1], err)
}
envMTD = &mtdInfo{
dev: "/dev/" + m[1],
sizeHex: "0x" + strings.ToLower(m[2]),
eraseHex: "0x" + strings.ToLower(m[3]),
name: m[4],
partSize: partSize,
eraseSize: eraseSize,
}
break
}
if envMTD == nil {
return fmt.Errorf("no %q partition found in %s", targetName, procMTDPath)
}
if _, err := exec.LookPath("fw_printenv"); err != nil {
return fmt.Errorf("fw_printenv not found in PATH: %w", err)
}
// Candidate env sizes to probe, smallest first.
// Many boards use 0x2000/0x4000/0x8000/0x10000.
// Keep candidates <= partition size and > 0.
candidateSizes := uniqueUint64s([]uint64{
0x2000,
0x4000,
0x8000,
0x10000,
envMTD.eraseSize,
})
var filtered []uint64
for _, sz := range candidateSizes {
if sz > 0 && sz <= envMTD.partSize {
filtered = append(filtered, sz)
}
}
if len(filtered) == 0 {
return fmt.Errorf("no valid candidate env sizes for %s", envMTD.dev)
}
type probeResult struct {
size uint64
config string
output string
}
var best *probeResult
var attempts []probeResult
for _, sz := range filtered {
cfg := fmt.Sprintf("%s 0x0 0x%x 0x%x\n", envMTD.dev, sz, envMTD.eraseSize)
klog.Infof("Trying: %s", cfg)
tmpDir, err := os.MkdirTemp("", "fwenv-probe-*")
if err != nil {
return fmt.Errorf("create temp dir: %w", err)
}
cfgPath := filepath.Join(tmpDir, "fw_env.config")
if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil {
_ = os.RemoveAll(tmpDir)
return fmt.Errorf("write temp config: %w", err)
}
out, _ := runFWPrintenvWithConfig(ctx, cfgPath)
_ = os.RemoveAll(tmpDir)
res := probeResult{
size: sz,
config: cfg,
output: out,
}
attempts = append(attempts, res)
if looksLikeValidUBootEnv(out) {
best = &res
break
}
}
if best == nil {
var b strings.Builder
b.WriteString("could not determine correct fw_env.config by probing candidate sizes\n")
b.WriteString(fmt.Sprintf("partition: %s size=%s erase=%s\n", envMTD.dev, envMTD.sizeHex, envMTD.eraseHex))
for _, a := range attempts {
b.WriteString(fmt.Sprintf("\n--- candidate env-size=0x%x ---\n", a.size))
b.WriteString(trimOutput(a.output, 600))
b.WriteString("\n")
}
return fmt.Errorf(b.String())
}
klog.Infof("Using: %s", best.config)
// Avoid rewriting if already identical.
existing, err := os.ReadFile(fwEnvCfgPath)
if err == nil && bytes.Equal(existing, []byte(best.config)) {
return nil
}
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read %s: %w", fwEnvCfgPath, err)
}
if err := os.MkdirAll(filepath.Dir(fwEnvCfgPath), 0o755); err != nil {
return fmt.Errorf("mkdir %s: %w", filepath.Dir(fwEnvCfgPath), err)
}
if err := os.WriteFile(fwEnvCfgPath, []byte(best.config), 0o644); err != nil {
return fmt.Errorf("write %s: %w", fwEnvCfgPath, err)
}
return nil
}
func runFWPrintenvWithConfig(ctx context.Context, configPath string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "fw_printenv", "-c", configPath)
out, err := cmd.CombinedOutput()
return string(out), err
}
func looksLikeValidUBootEnv(out string) bool {
lower := strings.ToLower(out)
// Hard fail patterns.
if strings.Contains(lower, "bad crc") {
return false
}
if strings.Contains(lower, "using default environment") {
return false
}
if strings.Contains(lower, "cannot parse config file") {
return false
}
if strings.Contains(lower, "failed to find nvmem device") {
return false
}
if strings.Contains(lower, "configuration file wrong") {
return false
}
// Need a few plausible env variables.
lines := strings.Split(out, "\n")
var hits int
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.Contains(line, "=") {
continue
}
switch {
case strings.HasPrefix(line, "bootcmd="),
strings.HasPrefix(line, "bootdelay="),
strings.HasPrefix(line, "baudrate="),
strings.HasPrefix(line, "bootargs="),
strings.HasPrefix(line, "altbootcmd="),
strings.HasPrefix(line, "stderr="),
strings.HasPrefix(line, "stdin="),
strings.HasPrefix(line, "stdout="):
hits++
}
}
return hits >= 2
}
func uniqueUint64s(in []uint64) []uint64 {
seen := make(map[uint64]struct{}, len(in))
out := make([]uint64, 0, len(in))
for _, v := range in {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}
func trimOutput(s string, max int) string {
s = strings.TrimSpace(s)
if len(s) <= max {
return s
}
return s[:max] + "...(truncated)"
}