penguin/monok8s

k8s image for Mono Gateway Dev Kit

clitools/pkg/node/uboot/init.go

raw ยท 5973 bytes

package uboot

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"k8s.io/klog/v2"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"example.com/monok8s/pkg/node"
)

func ConfigureUBootCommands(ctx context.Context, n *node.NodeContext) error {
	_ = 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 errors.New(b.String())
	}

	klog.Infof("Using: %s", best.config)

	// Avoid rewriting if already identical.
	existing, err := os.ReadFile(HostFWEnvCfgPath)
	if err == nil && bytes.Equal(existing, []byte(best.config)) {
		return nil
	}
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("read %s: %w", HostFWEnvCfgPath, err)
	}

	if err := os.MkdirAll(filepath.Dir(HostFWEnvCfgPath), 0o755); err != nil {
		return fmt.Errorf("mkdir %s: %w", filepath.Dir(HostFWEnvCfgPath), err)
	}
	if err := os.WriteFile(HostFWEnvCfgPath, []byte(best.config), 0o644); err != nil {
		return fmt.Errorf("write %s: %w", HostFWEnvCfgPath, 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)"
}