Writes bootcmd
This commit is contained in:
30
clitools/pkg/node/uboot/ab.go
Normal file
30
clitools/pkg/node/uboot/ab.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package uboot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"example.com/monok8s/pkg/node"
|
||||
)
|
||||
|
||||
func ConfigureABBoot(ctx context.Context, nctx *node.NodeContext) error {
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current executable path: %w", err)
|
||||
}
|
||||
|
||||
writer := NewFWEnvWriter(HostFWEnvCfgPath, exePath)
|
||||
|
||||
// TODO: configurable from cluster.env
|
||||
return writer.EnsureBootEnv(ctx, BootEnvConfig{
|
||||
BootSource: "usb",
|
||||
BootPart: "A",
|
||||
BootDisk: 0,
|
||||
RootfsAPartNum: 2,
|
||||
RootfsBPartNum: 3,
|
||||
DataPartNum: 4,
|
||||
LinuxRootPrefix: "/dev/sda",
|
||||
})
|
||||
}
|
||||
60
clitools/pkg/node/uboot/bootenvconfig.go
Normal file
60
clitools/pkg/node/uboot/bootenvconfig.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package uboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c BootEnvConfig) Validate() error {
|
||||
switch c.BootSource {
|
||||
case "usb", "emmc":
|
||||
default:
|
||||
return fmt.Errorf("invalid boot source %q", c.BootSource)
|
||||
}
|
||||
|
||||
switch c.BootPart {
|
||||
case "A", "B":
|
||||
default:
|
||||
return fmt.Errorf("invalid boot part %q", c.BootPart)
|
||||
}
|
||||
|
||||
if c.BootDisk < 0 {
|
||||
return fmt.Errorf("invalid boot disk %d", c.BootDisk)
|
||||
}
|
||||
if c.RootfsAPartNum <= 0 {
|
||||
return fmt.Errorf("invalid rootfs A part %d", c.RootfsAPartNum)
|
||||
}
|
||||
if c.RootfsBPartNum <= 0 {
|
||||
return fmt.Errorf("invalid rootfs B part %d", c.RootfsBPartNum)
|
||||
}
|
||||
if c.DataPartNum <= 0 {
|
||||
return fmt.Errorf("invalid data part %d", c.DataPartNum)
|
||||
}
|
||||
if strings.TrimSpace(c.LinuxRootPrefix) == "" {
|
||||
return fmt.Errorf("linux root prefix is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BootEnvConfig) bootCmdOrDefault() string {
|
||||
if s := strings.TrimSpace(c.BootCmd); s != "" {
|
||||
return s
|
||||
}
|
||||
return compactUBootScript(defaultBootCmdTemplate)
|
||||
}
|
||||
|
||||
func compactUBootScript(s string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
143
clitools/pkg/node/uboot/env_writer.go
Normal file
143
clitools/pkg/node/uboot/env_writer.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package uboot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"example.com/monok8s/pkg/system"
|
||||
)
|
||||
|
||||
func NewFWEnvWriter(configPath string, ctlPath string) *FWEnvWriter {
|
||||
return &FWEnvWriter{
|
||||
Runner: system.NewRunner(system.RunnerConfig{
|
||||
DefaultTimeout: 15 * time.Second,
|
||||
StreamOutput: false,
|
||||
Logger: &system.StdLogger{},
|
||||
}),
|
||||
ConfigPath: configPath,
|
||||
CtlPath: ctlPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *FWEnvWriter) GetEnv(ctx context.Context, key string) (string, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("key is required")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"internal", "fw-printenv",
|
||||
"--key", key,
|
||||
"--config", w.ConfigPath,
|
||||
}
|
||||
|
||||
res, err := w.Runner.RunWithOptions(ctx, w.CtlPath, args, system.RunOptions{Quiet: true})
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
return "", fmt.Errorf("fw-printenv %q: %w (stdout=%q stderr=%q)",
|
||||
key, err, strings.TrimSpace(res.Stdout), strings.TrimSpace(res.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("fw-printenv %q: %w", key, err)
|
||||
}
|
||||
|
||||
out := strings.TrimSpace(res.Stdout)
|
||||
if out == "" {
|
||||
return "", fmt.Errorf("empty output for key %q", key)
|
||||
}
|
||||
|
||||
parts := strings.SplitN(out, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("unexpected fw_printenv output for %q: %q", key, out)
|
||||
}
|
||||
if parts[0] != key {
|
||||
return "", fmt.Errorf("unexpected fw_printenv key: got %q want %q", parts[0], key)
|
||||
}
|
||||
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
func (w *FWEnvWriter) SetEnv(ctx context.Context, key, value string) error {
|
||||
key = strings.TrimSpace(key)
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("key is required")
|
||||
}
|
||||
if value == "" {
|
||||
return fmt.Errorf("value is required")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"internal", "fw-setenv",
|
||||
"--key", key,
|
||||
"--value", value,
|
||||
"--config", w.ConfigPath,
|
||||
}
|
||||
|
||||
res, err := w.Runner.RunWithOptions(ctx, w.CtlPath, args, system.RunOptions{Quiet: true})
|
||||
if err != nil {
|
||||
if res != nil {
|
||||
return fmt.Errorf("fw-setenv %q: %w (stdout=%q stderr=%q)",
|
||||
key, err, strings.TrimSpace(res.Stdout), strings.TrimSpace(res.Stderr))
|
||||
}
|
||||
return fmt.Errorf("fw-setenv %q: %w", key, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *FWEnvWriter) SetEnvIfDifferent(ctx context.Context, key, desired string) error {
|
||||
current, err := w.GetEnv(ctx, key)
|
||||
if err == nil && current == desired {
|
||||
klog.V(1).InfoS("fw env already matches", "key", key, "value", desired)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
klog.InfoS("fw env key missing or unreadable, will set",
|
||||
"key", key,
|
||||
"desired", desired,
|
||||
"error", err,
|
||||
)
|
||||
} else {
|
||||
klog.InfoS("fw env drift detected, updating",
|
||||
"key", key,
|
||||
"current", current,
|
||||
"desired", desired,
|
||||
)
|
||||
}
|
||||
|
||||
return w.SetEnv(ctx, key, desired)
|
||||
}
|
||||
|
||||
func (w *FWEnvWriter) EnsureBootEnv(ctx context.Context, cfg BootEnvConfig) error {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envs := []struct {
|
||||
Key string
|
||||
Value string
|
||||
}{
|
||||
{"bootcmd", cfg.bootCmdOrDefault()},
|
||||
{"boot_source", string(cfg.BootSource)},
|
||||
{"boot_part", string(cfg.BootPart)},
|
||||
{"boot_disk", fmt.Sprintf("%d", cfg.BootDisk)},
|
||||
{"rootfs_a_partnum", fmt.Sprintf("%d", cfg.RootfsAPartNum)},
|
||||
{"rootfs_b_partnum", fmt.Sprintf("%d", cfg.RootfsBPartNum)},
|
||||
{"data_partnum", fmt.Sprintf("%d", cfg.DataPartNum)},
|
||||
{"linux_root_prefix", cfg.LinuxRootPrefix},
|
||||
}
|
||||
|
||||
for _, kv := range envs {
|
||||
if err := w.SetEnvIfDifferent(ctx, kv.Key, kv.Value); err != nil {
|
||||
return fmt.Errorf("ensure %s: %w", kv.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
254
clitools/pkg/node/uboot/init.go
Normal file
254
clitools/pkg/node/uboot/init.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package uboot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"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 fmt.Errorf(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)"
|
||||
}
|
||||
60
clitools/pkg/node/uboot/types.go
Normal file
60
clitools/pkg/node/uboot/types.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package uboot
|
||||
|
||||
import (
|
||||
"example.com/monok8s/pkg/system"
|
||||
)
|
||||
|
||||
const (
|
||||
procMTDPath = "/proc/mtd"
|
||||
targetName = "uboot-env"
|
||||
|
||||
HostFWEnvCfgPath = "/etc/fw_env.config"
|
||||
)
|
||||
|
||||
type BootEnvConfig struct {
|
||||
BootSource string // usb or emmc
|
||||
BootPart string // A or B
|
||||
BootDisk int
|
||||
RootfsAPartNum int
|
||||
RootfsBPartNum int
|
||||
DataPartNum int
|
||||
LinuxRootPrefix string // /dev/sda or /dev/mmcblk0p
|
||||
BootCmd string // optional; defaults to DefaultBootCmd
|
||||
}
|
||||
|
||||
type FWEnvWriter struct {
|
||||
Runner *system.Runner
|
||||
ConfigPath string
|
||||
CtlPath string
|
||||
}
|
||||
|
||||
const defaultBootCmdTemplate = `
|
||||
if test "${boot_source}" = "usb"; then
|
||||
usb start || exit;
|
||||
setenv boot_iface usb;
|
||||
elif test "${boot_source}" = "emmc"; then
|
||||
mmc dev ${boot_disk} || exit;
|
||||
setenv boot_iface mmc;
|
||||
else
|
||||
echo "unsupported boot_source: ${boot_source}";
|
||||
exit;
|
||||
fi;
|
||||
|
||||
setenv kernel_addr_r 0xa0000000;
|
||||
|
||||
if test "${boot_part}" = "A"; then
|
||||
setenv rootpart ${rootfs_a_partnum};
|
||||
elif test "${boot_part}" = "B"; then
|
||||
setenv rootpart ${rootfs_b_partnum};
|
||||
else
|
||||
echo "unsupported boot_part: ${boot_part}";
|
||||
exit;
|
||||
fi;
|
||||
|
||||
setenv bootdev ${boot_disk}:${rootpart};
|
||||
setenv rootdev ${linux_root_prefix}${rootpart};
|
||||
setenv datadev ${linux_root_prefix}${data_partnum};
|
||||
|
||||
setenv bootargs "${bootargs_console} root=${rootdev} data=${datadev} rw rootwait rootfstype=ext4";
|
||||
ext4load ${boot_iface} ${bootdev} ${kernel_addr_r} /boot/kernel.itb && bootm ${kernel_addr_r};
|
||||
`
|
||||
Reference in New Issue
Block a user