commit 50d9440e0ab13b531aa8a50c973916c7b2958944f9881d340091377be7b279d7
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-04-05T18:20:41Z |
| subject | Reduce the number of bootenv vars |
commit 50d9440e0ab13b531aa8a50c973916c7b2958944f9881d340091377be7b279d7
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-04-05T18:20:41Z
Reduce the number of bootenv vars
---
README.md | 2 +-
build.env | 4 +-
clitools/pkg/controller/osupgrade/handler.go | 14 +-
clitools/pkg/node/agent.go | 10 ++
clitools/pkg/node/uboot/ab.go | 41 ++++-
clitools/pkg/node/uboot/bootcmd_test.go | 12 ++
clitools/pkg/node/uboot/bootenvconfig.go | 16 --
clitools/pkg/node/uboot/env_writer.go | 5 -
clitools/pkg/node/uboot/init.go | 3 +-
clitools/pkg/node/uboot/types.go | 49 +++---
configs/cluster.env.default | 12 ++
docs/ota.md | 11 +-
initramfs/rootfs-extra/init | 219 ++++++++++++++++++++++++---
macos/flashusb.sh | 150 ++++++++++++++++--
14 files changed, 455 insertions(+), 93 deletions(-)
diff --git a/README.md b/README.md
index 5895fad..45b45d5 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ https://docs.mono.si/gateway-development-kit/getting-started
## IMPORTANT NOTES
* This is not your everyday linux image! It is best suited for users that is already familiar
- with k8s. For first-timers, you may want to try the default config that gives you a ready-to-used
+ with k8s. For first-timers, you may want to try the default config that gives you a ready-to-use
cluster then get yourself started from there
* The 3 RJ45 ports are label in eth1, eth2, eth0 respectively by the kernel (left to right)
diff --git a/build.env b/build.env
index d573be2..ecffdcb 100644
--- a/build.env
+++ b/build.env
@@ -5,8 +5,8 @@ TAG=dev
# The Linux kernel, from NXP
NXP_VERSION=lf-6.18.2-1.0.0
-CRIO_VERSION=cri-o.arm64.v1.35.2
-KUBE_VERSION=v1.34.1
+CRIO_VERSION=cri-o.arm64.v1.33.3
+KUBE_VERSION=v1.33.3
# Mono's tutorial said fsl-ls1046a-rdb.dtb but our shipped board is not that one
# We need fsl-ls1046a-rdb-sdk.dtb here
diff --git a/clitools/pkg/controller/osupgrade/handler.go b/clitools/pkg/controller/osupgrade/handler.go
index 51a519a..8cd16f3 100644
--- a/clitools/pkg/controller/osupgrade/handler.go
+++ b/clitools/pkg/controller/osupgrade/handler.go
@@ -3,6 +3,7 @@ package osupgrade
import (
"context"
"fmt"
+ "os"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
@@ -12,6 +13,7 @@ import (
"example.com/monok8s/pkg/catalog"
"example.com/monok8s/pkg/controller/osimage"
"example.com/monok8s/pkg/kube"
+ "example.com/monok8s/pkg/node/uboot"
)
func HandleOSUpgrade(ctx context.Context, clients *kube.Clients,
@@ -89,7 +91,7 @@ func HandleOSUpgrade(ctx context.Context, clients *kube.Clients,
imageOptions := osimage.ApplyOptions{
URL: first.URL,
- TargetPath: "./out/flash.img",
+ TargetPath: "/dev/sda?",
ExpectedRawSHA256: imageSHA,
ExpectedRawSize: first.Size,
BufferSize: 6 * 1024 * 1024,
@@ -128,10 +130,9 @@ func HandleOSUpgrade(ctx context.Context, clients *kube.Clients,
}
klog.Info(result)
- if err := SetNextBootEnv(ctx, NextBootConfig{
- Key: "boot_part",
- Value: "B",
- }); err != nil {
+
+ cfgPath := os.Getenv("FW_ENV_CONFIG_FILE")
+ if err := uboot.ConfigureNextBoot(ctx, cfgPath); err != nil {
return failProgress(ctx, clients, osup, "set boot env", err)
}
@@ -145,5 +146,8 @@ func HandleOSUpgrade(ctx context.Context, clients *kube.Clients,
return fmt.Errorf("update progress status: %w", err)
}
+ // TODO: Drain the node here
+ // TODO: Issue Reboot
+
return nil
}
diff --git a/clitools/pkg/node/agent.go b/clitools/pkg/node/agent.go
index d52d63f..924c1ab 100644
--- a/clitools/pkg/node/agent.go
+++ b/clitools/pkg/node/agent.go
@@ -118,6 +118,16 @@ func applyControlAgentClusterRole(ctx context.Context, kubeClient kubernetes.Int
Resources: []string{"osupgrades/status"},
Verbs: []string{"get", "patch", "update"},
},
+ {
+ APIGroups: []string{monov1alpha1.Group},
+ Resources: []string{"osupgradeprogresses"},
+ Verbs: []string{"get", "list", "watch", "create", "patch", "update"},
+ },
+ {
+ APIGroups: []string{monov1alpha1.Group},
+ Resources: []string{"osupgradeprogresses/status"},
+ Verbs: []string{"get", "list", "watch", "create", "patch", "update"},
+ },
{
APIGroups: []string{""},
Resources: []string{"nodes"},
diff --git a/clitools/pkg/node/uboot/ab.go b/clitools/pkg/node/uboot/ab.go
index 80cf256..cde8680 100644
--- a/clitools/pkg/node/uboot/ab.go
+++ b/clitools/pkg/node/uboot/ab.go
@@ -19,12 +19,39 @@ func ConfigureABBoot(ctx context.Context, nctx *node.NodeContext) error {
// 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",
+ BootSource: "usb",
+ BootPart: "A",
+ })
+}
+
+// This is called by the agent controller/osupgrade/handler.go
+func ConfigureNextBoot(ctx context.Context, fwEnvCfgPath string) error {
+
+ exePath, err := os.Executable()
+ if err != nil {
+ return fmt.Errorf("get current executable path: %w", err)
+ }
+
+ writer := NewFWEnvWriter(fwEnvCfgPath, exePath)
+
+ currBootPart, err := writer.GetEnv(ctx, "boot_part")
+ if err != nil {
+ return fmt.Errorf("get boot_part: %w", err)
+ }
+
+ next := "A"
+ if currBootPart == "A" {
+ next = "B"
+
+ }
+
+ currBootSource, err := writer.GetEnv(ctx, "boot_source")
+ if err != nil {
+ return fmt.Errorf("get boot_source: %w", err)
+ }
+
+ return writer.EnsureBootEnv(ctx, BootEnvConfig{
+ BootSource: currBootSource,
+ BootPart: next,
})
}
diff --git a/clitools/pkg/node/uboot/bootcmd_test.go b/clitools/pkg/node/uboot/bootcmd_test.go
new file mode 100644
index 0000000..5a0cf1e
--- /dev/null
+++ b/clitools/pkg/node/uboot/bootcmd_test.go
@@ -0,0 +1,12 @@
+package uboot
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestBootCmd(_ *testing.T) {
+
+ cfg := BootEnvConfig{}
+ fmt.Println(cfg.bootCmdOrDefault())
+}
diff --git a/clitools/pkg/node/uboot/bootenvconfig.go b/clitools/pkg/node/uboot/bootenvconfig.go
index c1f0b20..0ce8781 100644
--- a/clitools/pkg/node/uboot/bootenvconfig.go
+++ b/clitools/pkg/node/uboot/bootenvconfig.go
@@ -18,22 +18,6 @@ func (c BootEnvConfig) Validate() error {
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
}
diff --git a/clitools/pkg/node/uboot/env_writer.go b/clitools/pkg/node/uboot/env_writer.go
index 0419af4..8cb9e32 100644
--- a/clitools/pkg/node/uboot/env_writer.go
+++ b/clitools/pkg/node/uboot/env_writer.go
@@ -126,11 +126,6 @@ func (w *FWEnvWriter) EnsureBootEnv(ctx context.Context, cfg BootEnvConfig) erro
{"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 {
diff --git a/clitools/pkg/node/uboot/init.go b/clitools/pkg/node/uboot/init.go
index 27affa7..43278ae 100644
--- a/clitools/pkg/node/uboot/init.go
+++ b/clitools/pkg/node/uboot/init.go
@@ -3,6 +3,7 @@ package uboot
import (
"bytes"
"context"
+ "errors"
"fmt"
"k8s.io/klog/v2"
"os"
@@ -155,7 +156,7 @@ func ConfigureUBootCommands(ctx context.Context, n *node.NodeContext) error {
b.WriteString(trimOutput(a.output, 600))
b.WriteString("\n")
}
- return fmt.Errorf(b.String())
+ return errors.New(b.String())
}
klog.Infof("Using: %s", best.config)
diff --git a/clitools/pkg/node/uboot/types.go b/clitools/pkg/node/uboot/types.go
index acf034f..fae78af 100644
--- a/clitools/pkg/node/uboot/types.go
+++ b/clitools/pkg/node/uboot/types.go
@@ -12,14 +12,9 @@ const (
)
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
+ BootSource string // usb or emmc
+ BootPart string // A or B
+ BootCmd string // optional; defaults to DefaultBootCmd
}
type FWEnvWriter struct {
@@ -29,32 +24,52 @@ type FWEnvWriter struct {
}
const defaultBootCmdTemplate = `
+setenv kernel_addr_r 0xa0000000;
+
+if usb start; then
+ if usb dev 0; then
+ echo "Trying generic USB boot...";
+
+ # Prefer extlinux-style boot
+ if test -e usb 0:1 /boot/extlinux/extlinux.conf; then
+ echo "Found extlinux config on USB";
+ sysboot usb 0:1 any ${kernel_addr_r} /boot/extlinux/extlinux.conf;
+ fi;
+
+ # Fallback: U-Boot script image
+ if test -e usb 0:1 /boot/boot.scr; then
+ echo "Found boot.scr on USB";
+ if load usb 0:1 ${kernel_addr_r} /boot/boot.scr; then
+ source ${kernel_addr_r};
+ fi;
+ fi;
+ fi;
+fi;
+
if test "${boot_source}" = "usb"; then
usb start || exit;
setenv boot_iface usb;
+ usb dev 0 || exit;
elif test "${boot_source}" = "emmc"; then
- mmc dev ${boot_disk} || exit;
+ mmc dev 0 || 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};
+ setenv rootpart 2;
elif test "${boot_part}" = "B"; then
- setenv rootpart ${rootfs_b_partnum};
+ setenv rootpart 3;
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 bootdev 0:${rootpart};
+setenv rootdev ${boot_source}:${rootpart};
-setenv bootargs "${bootargs_console} root=${rootdev} data=${datadev} rw rootwait rootfstype=ext4";
+setenv bootargs "${bootargs_console} root=${rootdev} bootpart=${boot_part} rw rootwait rootfstype=ext4";
ext4load ${boot_iface} ${bootdev} ${kernel_addr_r} /boot/kernel.itb && bootm ${kernel_addr_r};
`
diff --git a/configs/cluster.env.default b/configs/cluster.env.default
index b03c5a8..fa65f24 100644
--- a/configs/cluster.env.default
+++ b/configs/cluster.env.default
@@ -33,6 +33,18 @@ MKS_INIT_CONTROL_PLANE=yes
# OSUpgrade agent
MKS_ENABLE_CONTROL_AGENT=yes
+# Boot configs
+# usb, emmc
+MKS_BOOT_SOURCE="usb"
+MKS_BOOT_DEV_PREFIX="/dev/sda"
+
+# Depends on your full disk image. This is the default
+MKS_BOOT_PART=A
+MKS_BOOT_DISK=0
+MKS_ROOTFS_PART_A=2
+MKS_ROOTFS_PART_B=3
+MKS_ROOTFS_PART_D=4
+
MKS_API_SERVER_ENDPOINT=
MKS_BOOTSTRAP_TOKEN=
MKS_DISCOVERY_TOKEN_CA_CERT_HASH=
diff --git a/docs/ota.md b/docs/ota.md
index 2f4f41a..deacf80 100644
--- a/docs/ota.md
+++ b/docs/ota.md
@@ -1,10 +1,12 @@
## Upgrade process
-We use a CRD with an agent to handle this. Our versions follows upstream's.
+We use an agent to watch the OSUpgrade CRD to handle this. Our image versions follows upstream.
To issue an upgrade. Simply use
kubectl apply -f upgrade.yaml
+
+Example yaml
```yaml
apiVersion: monok8s.io/v1alpha1
kind: OSUpgrade
@@ -12,8 +14,6 @@ metadata:
name: "my-ugrade-2"
spec:
version: "v1.35.3"
- imageURL: "https://updates.example.com/monok8s-1.2.3.img.zst"
- checksum: "sha256:..."
nodeSelector: {}
catalog:
inline: |
@@ -39,7 +39,7 @@ spec:
- v1.34.0
```
-catalog accepts URL or ConfigMap
+catalog also accepts URL or ※ConfigMap
```yaml
catalog:
URL: https://example.com/images.yaml
@@ -48,6 +48,9 @@ catalog:
ConfigMap: images-cm
```
+※ ConfigMap requires additional RBAC permission which is not by default. You edit the
+control-agent ClusterRole and add `cnofigmaps: get` to allow this.
+
Contents should look like this
```yaml
stable: v1.35.1
diff --git a/initramfs/rootfs-extra/init b/initramfs/rootfs-extra/init
index c5027de..aa76fa6 100755
--- a/initramfs/rootfs-extra/init
+++ b/initramfs/rootfs-extra/init
@@ -42,6 +42,168 @@ wait_for_path() {
done
}
+get_cmdline_arg() {
+ key="$1"
+ for arg in $(cat /proc/cmdline); do
+ case "$arg" in
+ "$key"=*)
+ echo "${arg#"$key"=}"
+ return 0
+ ;;
+ esac
+ done
+ return 1
+}
+
+# Read KEY=VALUE pairs from /sys/class/block/*/uevent without spawning grep/cut.
+get_uevent_value() {
+ file="$1"
+ want_key="$2"
+
+ [ -f "$file" ] || return 1
+
+ while IFS='=' read -r k v; do
+ [ "$k" = "$want_key" ] && {
+ echo "$v"
+ return 0
+ }
+ done < "$file"
+
+ return 1
+}
+
+# Return the /dev/<partition> path for the first partition whose GPT PARTNAME matches.
+find_first_part_by_partname() {
+ want_label="$1"
+
+ for p in /sys/class/block/*; do
+ [ -f "$p/partition" ] || continue
+
+ partname="$(get_uevent_value "$p/uevent" PARTNAME || true)"
+ [ "$partname" = "$want_label" ] || continue
+
+ devname="$(basename "$p")"
+ echo "/dev/$devname"
+ return 0
+ done
+
+ return 1
+}
+
+wait_for_partnames() {
+ timeout="${1:-3}"
+ shift
+
+ i=0
+ while [ "$i" -lt "$timeout" ]; do
+ all_found=1
+ for name in "$@"; do
+ if ! find_first_part_by_partname "$name" >/dev/null; then
+ all_found=0
+ break
+ fi
+ done
+
+ [ "$all_found" -eq 1 ] && return 0
+
+ sleep 1
+ i=$((i + 1))
+ log "Still waiting for $@ to populate($i)"
+ done
+
+ return 1
+}
+
+find_part_by_partuuid() {
+ want="$1"
+
+ for p in /sys/class/block/*; do
+ [ -f "$p/partition" ] || continue
+
+ partuuid="$(get_uevent_value "$p/uevent" PARTUUID || true)"
+ [ "$partuuid" = "$want" ] || continue
+
+ echo "/dev/$(basename "$p")"
+ return 0
+ done
+
+ return 1
+}
+
+# Return the parent disk name for a partition device name.
+# Examples:
+# sda2 -> sda
+# mmcblk0p2 -> mmcblk0
+parent_disk_name_for_part() {
+ part_devname="$1"
+
+ real="$(readlink -f "/sys/class/block/$part_devname")" || return 1
+ parent="$(basename "$(dirname "$real")")" || return 1
+
+ echo "$parent"
+ return 0
+}
+
+# Find a sibling partition on the same disk by GPT PARTNAME.
+find_sibling_part_on_same_disk() {
+ part_path="$1"
+ want_label="$2"
+
+ part_devname="$(basename "$part_path")"
+ disk_devname="$(parent_disk_name_for_part "$part_devname")" || return 1
+
+ for p in /sys/class/block/"$disk_devname"*; do
+ [ -f "$p/partition" ] || continue
+
+ partname="$(get_uevent_value "$p/uevent" PARTNAME || true)"
+ [ "$partname" = "$want_label" ] || continue
+
+ echo "/dev/$(basename "$p")"
+ return 0
+ done
+
+ return 1
+}
+
+# Resolve preferred root device from sysfs.
+# Prefer PARTUUID first, then optionally filesystem UUID if explicitly provided.
+resolve_preferred_root() {
+ pref_root="$1"
+
+ [ -n "$pref_root" ] || return 1
+ find_part_by_partuuid "$pref_root"
+}
+
+# Decide which root PARTNAME we want for the requested slot.
+# Keep a compatibility fallback for legacy "rootfs" as slot A.
+wanted_root_labels_for_slot() {
+ slot="$1"
+
+ case "$slot" in
+ B|b)
+ echo "rootfsB"
+ ;;
+ *)
+ # Try modern rootfsA first, then legacy rootfs
+ echo "rootfsA rootfs"
+ ;;
+ esac
+}
+
+find_fallback_root_for_slot() {
+ slot="$1"
+
+ for label in $(wanted_root_labels_for_slot "$slot"); do
+ dev="$(find_first_part_by_partname "$label" || true)"
+ if [ -n "$dev" ]; then
+ echo "$dev"
+ return 0
+ fi
+ done
+
+ return 1
+}
+
mkdir -p /dev /proc /sys /run
mount_or_panic -t devtmpfs devtmpfs /dev
mount_or_panic -t proc proc /proc
@@ -62,45 +224,56 @@ log "Booting kernel took $(cut -d' ' -f1 /proc/uptime) seconds."
. /etc/build-info || panic "failed to source /etc/build-info"
-ROOT_DEV=""
-DATA_DEV=""
+wait_for_partnames 30 rootfsA rootfsB data || panic "failed to wait for fs"
-for arg in $(cat /proc/cmdline); do
- case "$arg" in
- root=*)
- ROOT_DEV="${arg#root=}"
- ;;
- data=*)
- DATA_DEV="${arg#data=}"
- ;;
- esac
-done
+ROOT_CMD="$(get_cmdline_arg root || true)"
+BOOT_PART="$(get_cmdline_arg bootpart || true)"
+PREFERRED_PARTUUID="$(get_cmdline_arg pref_root || true)"
-[ -n "$ROOT_DEV" ] || {
- log "No root= specified in cmdline"
- exec sh
-}
+ROOT_DEV="$(resolve_preferred_root "$PREFERRED_PARTUUID" || true)"
+if [ -n "$ROOT_DEV" ]; then
+ log "Using preferred root device: $ROOT_DEV"
+fi
-[ -n "$DATA_DEV" ] || {
- log "No data= specified in cmdline"
- exec sh
-}
+if [ -z "$ROOT_DEV" ]; then
+ ROOT_DEV="$(find_fallback_root_for_slot "$BOOT_PART" || true)"
+ if [ -n "$ROOT_DEV" ]; then
+ log "Preferred root not found. Falling back to first valid root device: $ROOT_DEV"
+ fi
+fi
+
+[ -n "$ROOT_DEV" ] || panic "no usable root device found"
+DATA_DEV="$(find_sibling_part_on_same_disk "$ROOT_DEV" data || true)"
+
+[ -n "$DATA_DEV" ] || panic "no data partition found on same disk as $ROOT_DEV"
+
+wait_for_path "$ROOT_DEV"
wait_for_path "$DATA_DEV"
e2fsck -p "$DATA_DEV" || {
- echo "Auto fsck failed, forcing repair"
+ log "Auto fsck failed, forcing repair"
e2fsck -y "$DATA_DEV" || panic "fsck failed on $DATA_DEV"
}
mkdir -p /newroot
+mkdir -p /newroot/data
+mkdir -p /newroot/var
+
mount_retry "$ROOT_DEV" /newroot ext4 ro
mount_retry "$DATA_DEV" /newroot/data ext4 rw
+mkdir -p /newroot/data/var
+mkdir -p /newroot/data/etc-overlay/upper
+mkdir -p /newroot/data/etc-overlay/work
+
mount_or_panic --bind /newroot/data/var /newroot/var
+# BusyBox mount just needs a normal -o option string here.
+# The important bit is that overlayfs itself requires lowerdir/upperdir/workdir,
+# and workdir must live on the same filesystem as upperdir.
mount_or_panic -t overlay overlay \
- -o lowerdir=/newroot/etc,upperdir=/newroot/data/etc-overlay/upper,workdir=/newroot/data/etc-overlay/work \
+ -o "lowerdir=/newroot/etc,upperdir=/newroot/data/etc-overlay/upper,workdir=/newroot/data/etc-overlay/work" \
/newroot/etc
mount_or_panic --move /dev /newroot/dev
@@ -108,7 +281,7 @@ mount_or_panic --move /proc /newroot/proc
mount_or_panic --move /sys /newroot/sys
mount_or_panic --move /run /newroot/run
-log "Switching root to $ROOT_DEV"
+log "Switching root to $ROOT_DEV (data: $DATA_DEV, slot: $BOOT_PART)"
exec switch_root /newroot /sbin/init
panic "switch_root returned unexpectedly"
diff --git a/macos/flashusb.sh b/macos/flashusb.sh
index 53804fa..2d39d29 100755
--- a/macos/flashusb.sh
+++ b/macos/flashusb.sh
@@ -2,10 +2,9 @@
set -eu
### User-configurable vars
-IMG="./out/monok8s-dev.img.gz"
-DEFAULT_DISK="/dev/disk7" # whole disk, not a partition
+IMG_DIR="./out"
CONFIG_PART_SUFFIX="s1" # config partition after flashing
-ENV_SEARCH_DIR="./out" # optional; leave empty to use image dir
+ENV_SEARCH_DIR="./out" # optional; leave empty to use selected image dir
MNT="/tmp/monok8s-usb-config.$$"
@@ -59,6 +58,131 @@ wait_for_mountable_partition() {
return 1
}
+select_image() {
+ dir="$1"
+
+ [ -d "$dir" ] || die "Image directory not found: $dir"
+
+ OLD_IFS="${IFS}"
+ IFS='
+'
+ set -- $(find "$dir" -maxdepth 1 -type f -name '*.img.gz' | sort)
+ IFS="${OLD_IFS}"
+
+ [ "$#" -gt 0 ] || die "No .img.gz images found in $dir"
+
+ echo "Available images:"
+ n=1
+ for img in "$@"; do
+ size="$(stat -f%z "$img" 2>/dev/null || echo "?")"
+ echo " [$n] $(basename "$img") (${size} bytes)"
+ n=$((n + 1))
+ done
+
+ echo
+ printf "Select image number: "
+ read -r choice
+
+ case "$choice" in
+ ''|*[!0-9]*)
+ die "Invalid selection: $choice"
+ ;;
+ esac
+
+ n=1
+ for img in "$@"; do
+ if [ "$n" -eq "$choice" ]; then
+ SELECTED_IMG="$img"
+ return 0
+ fi
+ n=$((n + 1))
+ done
+
+ die "Selection out of range: $choice"
+}
+
+list_candidate_disks() {
+ found=0
+ for dev in /dev/disk*; do
+ base="$(basename "$dev")"
+
+ case "$base" in
+ disk[0-9])
+ ;;
+ *)
+ continue
+ ;;
+ esac
+
+ info="$(diskutil list "$dev" 2>/dev/null || true)"
+ [ -n "$info" ] || continue
+
+ # Skip Apple disks
+ echo "$info" | grep -q "APFS" && continue
+
+ info="$(diskutil info "$dev" 2>/dev/null || true)"
+ found=1
+
+ media_name="$(echo "$info" | sed -n 's/^ *Device \/ Media Name: *//p' | head -n1)"
+ protocol="$(echo "$info" | sed -n 's/^ *Protocol: *//p' | head -n1)"
+ size="$(echo "$info" | sed -n 's/^ *Disk Size: *//p' | head -n1)"
+
+ echo "$dev|$media_name|$protocol|$size"
+ done
+
+ [ "$found" -eq 1 ] || return 1
+ return 0
+}
+
+select_disk() {
+ candidates="$(list_candidate_disks || true)"
+
+ if [ -z "$candidates" ]; then
+ warn "No obvious external candidate disks found."
+ printf "Enter target disk manually (example: /dev/disk7): "
+ read -r TARGET_DISK
+ [ -n "$TARGET_DISK" ] || die "No disk entered"
+ return 0
+ fi
+
+ echo "Candidate target disks:"
+ n=1
+ echo "$candidates" | while IFS='|' read -r dev media protocol size; do
+ echo " [$n] $dev - $media - $protocol - $size"
+ n=$((n + 1))
+ done
+
+ echo
+ printf "Select disk number (or type a full /dev/diskN path): "
+ read -r choice
+
+ case "$choice" in
+ /dev/disk[0-9]*)
+ TARGET_DISK="$choice"
+ return 0
+ ;;
+ ''|*[!0-9]*)
+ die "Invalid selection: $choice"
+ ;;
+ esac
+
+ n=1
+ OLD_IFS="${IFS}"
+ IFS='
+'
+ for line in $candidates; do
+ if [ "$n" -eq "$choice" ]; then
+ TARGET_DISK="$(echo "$line" | cut -d'|' -f1)"
+ IFS="${OLD_IFS}"
+ return 0
+ fi
+ n=$((n + 1))
+ done
+ IFS="${OLD_IFS}"
+
+ die "Selection out of range: $choice"
+}
+
### Sanity checks
require_cmd diskutil
require_cmd gunzip
@@ -70,24 +194,26 @@ require_cmd cp
require_cmd sed
require_cmd stat
require_cmd pv
+require_cmd cut
+require_cmd grep
+require_cmd head
-printf "disk ($DEFAULT_DISK): "
-read -r TARGET_DISK
+select_image "$IMG_DIR"
+IMG="$SELECTED_IMG"
-if [ -z "$TARGET_DISK" ]; then
- TARGET_DISK="$DEFAULT_DISK"
-fi
+select_disk
TARGET_RAW_DISK="/dev/r$(basename "$TARGET_DISK")"
TARGET_BASENAME="$(basename "$TARGET_DISK")"
### Derived vars
TARGET_CONFIG_PART="/dev/${TARGET_BASENAME}${CONFIG_PART_SUFFIX}"
-IMG_DIR="$(cd "$(dirname "$IMG")" && pwd)"
-SEARCH_DIR="${ENV_SEARCH_DIR:-$IMG_DIR}"
-IMG_SIZE_BYTES="$(stat -f%z "$IMG")"
+IMG_DIR_ABS="$(cd "$(dirname "$IMG")" && pwd)"
+SEARCH_DIR="${ENV_SEARCH_DIR:-$IMG_DIR_ABS}"
[ -f "$IMG" ] || die "Image not found: $IMG"
+IMG_SIZE_BYTES="$(stat -f%z "$IMG")"
+
[ -b "$TARGET_DISK" ] || die "Target disk not found: $TARGET_DISK"
[ "$(id -u)" -eq 0 ] || die "Run this script with sudo"
@@ -122,7 +248,7 @@ diskutil unmountDisk force "$TARGET_DISK" || die "Failed to unmount $TARGET_DISK
log "Flashing image with progress..."
pv -s "$IMG_SIZE_BYTES" "$IMG" \
| gunzip \
- | dd of="$TARGET_RAW_DISK" bs=1m
+ | dd of="$TARGET_RAW_DISK" bs=4m
log "Syncing writes..."
sync