301 lines
6.8 KiB
Bash
Executable File
301 lines
6.8 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
### User-configurable vars
|
|
IMG_DIR="./out"
|
|
CONFIG_PART_SUFFIX="s1" # config partition after flashing
|
|
ENV_SEARCH_DIR="./out" # optional; leave empty to use selected image dir
|
|
|
|
MNT="/tmp/monok8s-usb-config.$$"
|
|
|
|
cleanup() {
|
|
if mount | grep -q "on ${MNT} "; then
|
|
diskutil unmount "$MNT" >/dev/null 2>&1 || true
|
|
fi
|
|
rmdir "$MNT" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
log() {
|
|
echo "[*] $*"
|
|
}
|
|
|
|
warn() {
|
|
echo "[!] $*" >&2
|
|
}
|
|
|
|
die() {
|
|
echo "[!] ERROR: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
|
}
|
|
|
|
wait_for_disk() {
|
|
dev="$1"
|
|
timeout="${2:-15}"
|
|
i=0
|
|
while [ "$i" -lt "$timeout" ]; do
|
|
[ -e "$dev" ] && return 0
|
|
i=$((i + 1))
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
wait_for_mountable_partition() {
|
|
dev="$1"
|
|
timeout="${2:-20}"
|
|
i=0
|
|
while [ "$i" -lt "$timeout" ]; do
|
|
if [ -e "$dev" ] && diskutil info "$dev" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
i=$((i + 1))
|
|
sleep 1
|
|
done
|
|
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
|
|
require_cmd dd
|
|
require_cmd sync
|
|
require_cmd mount
|
|
require_cmd find
|
|
require_cmd cp
|
|
require_cmd sed
|
|
require_cmd stat
|
|
require_cmd pv
|
|
require_cmd cut
|
|
require_cmd grep
|
|
require_cmd head
|
|
|
|
select_image "$IMG_DIR"
|
|
IMG="$SELECTED_IMG"
|
|
|
|
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_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"
|
|
|
|
case "$TARGET_DISK" in
|
|
/dev/disk[0-9]*)
|
|
;;
|
|
*)
|
|
die "TARGET_DISK must look like /dev/diskN"
|
|
;;
|
|
esac
|
|
|
|
mkdir -p "$MNT"
|
|
trap cleanup EXIT
|
|
|
|
log "Image: $IMG"
|
|
log "Compressed image size: $IMG_SIZE_BYTES bytes"
|
|
log "Target disk: $TARGET_DISK"
|
|
log "Raw target disk: $TARGET_RAW_DISK"
|
|
log "Expected config partition after flash: $TARGET_CONFIG_PART"
|
|
|
|
log "Target disk info:"
|
|
diskutil info "$TARGET_DISK" || die "Unable to inspect target disk"
|
|
|
|
echo
|
|
printf "Type 'YES' to continue: "
|
|
read -r CONFIRM
|
|
[ "$CONFIRM" = "YES" ] || die "Aborted."
|
|
|
|
log "Unmounting target disk..."
|
|
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=4m
|
|
|
|
log "Syncing writes..."
|
|
sync
|
|
|
|
log "Ejecting and re-reading disk..."
|
|
diskutil eject "$TARGET_DISK" >/dev/null 2>&1 || true
|
|
sleep 2
|
|
|
|
log "Waiting for disk to come back..."
|
|
if ! wait_for_disk "$TARGET_DISK" 15; then
|
|
warn "Disk node did not reappear quickly. You may need to replug the drive."
|
|
fi
|
|
|
|
log "Disk layout after flashing:"
|
|
diskutil list "$TARGET_DISK" || warn "Could not list flashed disk yet"
|
|
|
|
ENV_FILES="$(find "$SEARCH_DIR" -maxdepth 1 -type f -name '*.env' | sort || true)"
|
|
|
|
if [ -n "$ENV_FILES" ]; then
|
|
log "Found env files:"
|
|
echo "$ENV_FILES" | sed 's/^/ /'
|
|
|
|
log "Waiting for config partition..."
|
|
wait_for_mountable_partition "$TARGET_CONFIG_PART" 20 || die "Config partition not ready: $TARGET_CONFIG_PART"
|
|
|
|
log "Mounting flashed config partition..."
|
|
diskutil mount -mountPoint "$MNT" "$TARGET_CONFIG_PART" >/dev/null || die "Failed to mount $TARGET_CONFIG_PART"
|
|
|
|
log "Copying env files to config partition..."
|
|
echo "$ENV_FILES" | while IFS= read -r f; do
|
|
[ -n "$f" ] || continue
|
|
base="$(basename "$f")"
|
|
cp "$f" "$MNT/$base"
|
|
echo " copied: $base"
|
|
done
|
|
|
|
log "Syncing copied config..."
|
|
sync
|
|
|
|
log "Config partition contents:"
|
|
ls -la "$MNT" || true
|
|
|
|
log "Unmounting config partition..."
|
|
diskutil unmount "$MNT" >/dev/null || warn "Unmount failed for $MNT"
|
|
else
|
|
log "No .env files found in $SEARCH_DIR"
|
|
fi
|
|
|
|
log "Done."
|