penguin/monok8s

k8s image for Mono Gateway Dev Kit

commit abc749a4b05cc2336cd6789100499cf4b1801356a00ebd669b4592c615e6e957

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-04-04T14:47:51Z
subjectWrites bootcmd
commit abc749a4b05cc2336cd6789100499cf4b1801356a00ebd669b4592c615e6e957
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-04-04T14:47:51Z

    Writes bootcmd
---
 clitools/docker/ctl-agent.Dockerfile          |   2 +
 clitools/pkg/bootstrap/registry.go            |   4 +-
 clitools/pkg/bootstrap/runner.go              |   5 +
 clitools/pkg/cmd/internal/fwprintenv.go       |   2 +-
 clitools/pkg/cmd/internal/fwsetenv.go         |   4 +-
 clitools/pkg/node/uboot/ab.go                 |  30 ++++++
 clitools/pkg/node/uboot/bootenvconfig.go      |  60 +++++++++++
 clitools/pkg/node/uboot/env_writer.go         | 143 ++++++++++++++++++++++++++
 clitools/pkg/node/{uboot.go => uboot/init.go} |  24 ++---
 clitools/pkg/node/uboot/types.go              |  60 +++++++++++
 10 files changed, 316 insertions(+), 18 deletions(-)

diff --git a/clitools/docker/ctl-agent.Dockerfile b/clitools/docker/ctl-agent.Dockerfile
index 7387bf2..79766a0 100644
--- a/clitools/docker/ctl-agent.Dockerfile
+++ b/clitools/docker/ctl-agent.Dockerfile
@@ -11,5 +11,7 @@ COPY out/fw_printenv ./
 COPY out/fw_setenv ./
 COPY --from=cacerts /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
 
+ENV PATH=/
+
 ENTRYPOINT ["/ctl"]
 CMD ["agent"]
diff --git a/clitools/pkg/bootstrap/registry.go b/clitools/pkg/bootstrap/registry.go
index 7bf8e34..5c4a52b 100644
--- a/clitools/pkg/bootstrap/registry.go
+++ b/clitools/pkg/bootstrap/registry.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"example.com/monok8s/pkg/node"
+	"example.com/monok8s/pkg/node/uboot"
 )
 
 type Registry struct {
@@ -30,7 +31,8 @@ func NewRegistry(ctx *node.NodeContext) *Registry {
 			"ConfigureDefaultCNI":                    node.ConfigureDefaultCNI,
 			"ConfigureHostname":                      node.ConfigureHostname(netCfg),
 			"ConfigureMgmtInterface":                 node.ConfigureMgmtInterface(netCfg),
-			"ConfigureUBootCommands":                 node.ConfigureUBootCommands,
+			"ConfigureABBoot":                        uboot.ConfigureABBoot,
+			"ConfigureUBootCommands":                 uboot.ConfigureUBootCommands,
 			"DetectLocalClusterState":                node.DetectLocalClusterState,
 			"EnsureIPForward":                        node.EnsureIPForward,
 			"ReconcileControlPlane":                  node.ReconcileControlPlane,
diff --git a/clitools/pkg/bootstrap/runner.go b/clitools/pkg/bootstrap/runner.go
index e57d461..d951046 100644
--- a/clitools/pkg/bootstrap/runner.go
+++ b/clitools/pkg/bootstrap/runner.go
@@ -137,6 +137,11 @@ func NewRunner(cfg *monov1alpha1.MonoKSConfig) *Runner {
 				Name:   "Ensure fw_env config and u-boot-tools availablilty",
 				Desc:   "Install or generate /etc/fw_env.config for U-Boot environment access",
 			},
+			{
+				RegKey: "ConfigureABBoot",
+				Name:   "Configure A/B booting environment",
+				Desc:   "Make A/B booting possible",
+			},
 			{
 				RegKey: "ApplyControlAgentDaemonSetResources",
 				Name:   "Apply daemonset for control agent",
diff --git a/clitools/pkg/cmd/internal/fwprintenv.go b/clitools/pkg/cmd/internal/fwprintenv.go
index 9bc6fde..74e710d 100644
--- a/clitools/pkg/cmd/internal/fwprintenv.go
+++ b/clitools/pkg/cmd/internal/fwprintenv.go
@@ -46,7 +46,7 @@ func newInternalFWPrintEnvCmd() *cobra.Command {
 
 			res, err := runner.RunWithOptions(
 				ctx,
-				"/fw_printenv",
+				"fw_printenv",
 				runArgs,
 				system.RunOptions{
 					Quiet: true,
diff --git a/clitools/pkg/cmd/internal/fwsetenv.go b/clitools/pkg/cmd/internal/fwsetenv.go
index 3537e33..fad5425 100644
--- a/clitools/pkg/cmd/internal/fwsetenv.go
+++ b/clitools/pkg/cmd/internal/fwsetenv.go
@@ -52,7 +52,7 @@ func newInternalFWSetEnvCmd() *cobra.Command {
 			// Preflight first so failure is clearer than blindly writing.
 			preflightRes, err := runner.RunWithOptions(
 				ctx,
-				"/fw_printenv",
+				"fw_printenv",
 				[]string{"-c", configPath},
 				system.RunOptions{
 					Quiet: true,
@@ -70,7 +70,7 @@ func newInternalFWSetEnvCmd() *cobra.Command {
 
 			res, err := runner.RunWithOptions(
 				ctx,
-				"/fw_setenv",
+				"fw_setenv",
 				[]string{
 					"-c", configPath,
 					key, value,
diff --git a/clitools/pkg/node/uboot/ab.go b/clitools/pkg/node/uboot/ab.go
new file mode 100644
index 0000000..80cf256
--- /dev/null
+++ b/clitools/pkg/node/uboot/ab.go
@@ -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",
+	})
+}
diff --git a/clitools/pkg/node/uboot/bootenvconfig.go b/clitools/pkg/node/uboot/bootenvconfig.go
new file mode 100644
index 0000000..c1f0b20
--- /dev/null
+++ b/clitools/pkg/node/uboot/bootenvconfig.go
@@ -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, " ")
+}
diff --git a/clitools/pkg/node/uboot/env_writer.go b/clitools/pkg/node/uboot/env_writer.go
new file mode 100644
index 0000000..0419af4
--- /dev/null
+++ b/clitools/pkg/node/uboot/env_writer.go
@@ -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
+}
diff --git a/clitools/pkg/node/uboot.go b/clitools/pkg/node/uboot/init.go
similarity index 90%
rename from clitools/pkg/node/uboot.go
rename to clitools/pkg/node/uboot/init.go
index f53bfd8..27affa7 100644
--- a/clitools/pkg/node/uboot.go
+++ b/clitools/pkg/node/uboot/init.go
@@ -1,4 +1,4 @@
-package node
+package uboot
 
 import (
 	"bytes"
@@ -11,15 +11,11 @@ import (
 	"regexp"
 	"strings"
 	"time"
-)
 
-func ConfigureUBootCommands(ctx context.Context, n *NodeContext) error {
-	const (
-		procMTDPath  = "/proc/mtd"
-		fwEnvCfgPath = "/etc/fw_env.config"
-		targetName   = "uboot-env"
-	)
+	"example.com/monok8s/pkg/node"
+)
 
+func ConfigureUBootCommands(ctx context.Context, n *node.NodeContext) error {
 	_ = n
 
 	type mtdInfo struct {
@@ -165,19 +161,19 @@ func ConfigureUBootCommands(ctx context.Context, n *NodeContext) error {
 	klog.Infof("Using: %s", best.config)
 
 	// Avoid rewriting if already identical.
-	existing, err := os.ReadFile(fwEnvCfgPath)
+	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", fwEnvCfgPath, err)
+		return fmt.Errorf("read %s: %w", HostFWEnvCfgPath, err)
 	}
 
-	if err := os.MkdirAll(filepath.Dir(fwEnvCfgPath), 0o755); err != nil {
-		return fmt.Errorf("mkdir %s: %w", filepath.Dir(fwEnvCfgPath), 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(fwEnvCfgPath, []byte(best.config), 0o644); err != nil {
-		return fmt.Errorf("write %s: %w", fwEnvCfgPath, err)
+	if err := os.WriteFile(HostFWEnvCfgPath, []byte(best.config), 0o644); err != nil {
+		return fmt.Errorf("write %s: %w", HostFWEnvCfgPath, err)
 	}
 
 	return nil
diff --git a/clitools/pkg/node/uboot/types.go b/clitools/pkg/node/uboot/types.go
new file mode 100644
index 0000000..acf034f
--- /dev/null
+++ b/clitools/pkg/node/uboot/types.go
@@ -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};
+`