From abc749a4b05cc2336cd6789100499cf4b1801356a00ebd669b4592c615e6e957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sat, 4 Apr 2026 22:47:51 +0800 Subject: [PATCH] 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(-) create mode 100644 clitools/pkg/node/uboot/ab.go create mode 100644 clitools/pkg/node/uboot/bootenvconfig.go create mode 100644 clitools/pkg/node/uboot/env_writer.go rename clitools/pkg/node/{uboot.go => uboot/init.go} (90%) create mode 100644 clitools/pkg/node/uboot/types.go 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" + + "example.com/monok8s/pkg/node" ) -func ConfigureUBootCommands(ctx context.Context, n *NodeContext) error { - const ( - procMTDPath = "/proc/mtd" - fwEnvCfgPath = "/etc/fw_env.config" - targetName = "uboot-env" - ) - +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}; +`