Initial RK1 media-center image project

This commit is contained in:
2026-08-17 18:42:29 +00:00
commit 5fe41e79e9
81 changed files with 5725 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env bash
# Move a running RK1 root filesystem from eMMC to a specifically identified
# NVMe while retaining U-Boot and /boot on eMMC.
set -Eeuo pipefail
umask 022
TARGET=""
EXPECTED_SERIAL=""
BOOT_ENV=/boot/armbianEnv.txt
BOOT_ENV_BACKUP=/boot/armbianEnv.txt.pre-nvme
INSTALL_LOG=/var/log/armbian-install.log
INSTALL_LOG_BACKUP=/var/log/armbian-install.log.pre-nvme
usage() {
cat <<'EOF'
Usage: sudo live-migrate-root-to-nvme.sh --target /dev/nvmeXnY --serial SERIAL
The target NVMe is erased. The running root must be on eMMC. U-Boot and /boot
remain on eMMC; only the root filesystem is moved to NVMe.
EOF
}
while (($#)); do
case "$1" in
--target) TARGET=${2:?}; shift 2 ;;
--serial) EXPECTED_SERIAL=${2:?}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ $EUID -eq 0 ]] || { echo "Run this script through sudo" >&2; exit 2; }
[[ -n "$TARGET" && -n "$EXPECTED_SERIAL" ]] || { usage >&2; exit 2; }
for command_name in armbian-install blkid chmod cmp cp debugfs dpkg dpkg-query \
e2fsck findmnt flock grep lsof lsblk ps readlink sed swapon sync systemctl \
udevadm umount xargs; do
command -v "$command_name" >/dev/null || {
echo "Missing required command: $command_name" >&2
exit 1
}
done
exec 9>/run/lock/rk1-nvme-migrate.lock
flock -n 9 || { echo "Another RK1 NVMe migration is already running" >&2; exit 1; }
TARGET=$(readlink -f -- "$TARGET")
[[ "$TARGET" =~ ^/dev/nvme[0-9]+n[0-9]+$ && -b "$TARGET" ]] || {
echo "Refusing non-NVMe whole-disk target: $TARGET" >&2
exit 1
}
[[ "$(lsblk -dnro TYPE "$TARGET")" == disk ]] || {
echo "Target is not a whole disk: $TARGET" >&2
exit 1
}
[[ "$(lsblk -dnro TRAN "$TARGET")" == nvme ]] || {
echo "Target transport is not NVMe: $TARGET" >&2
exit 1
}
actual_serial=$(lsblk -dnro SERIAL "$TARGET" | xargs)
actual_model=$(lsblk -dnro MODEL "$TARGET" | xargs)
[[ "$actual_serial" == "$EXPECTED_SERIAL" ]] || {
echo "Target serial mismatch: expected '$EXPECTED_SERIAL', found '$actual_serial'" >&2
exit 1
}
root_source=$(findmnt -no SOURCE --nofsroot /)
[[ "$root_source" =~ ^/dev/mmcblk[0-9]+p[0-9]+$ ]] || {
echo "Running root is not on an eMMC partition: $root_source" >&2
exit 1
}
root_disk="/dev/$(lsblk -no PKNAME "$root_source" | sed -n '1p')"
[[ -b "$root_disk" && "$root_disk" != "$TARGET" ]] || {
echo "Could not safely distinguish root disk from target" >&2
exit 1
}
target_mounts=$(lsblk -nrpo MOUNTPOINTS "$TARGET" | sed '/^[[:space:]]*$/d')
[[ -z "$target_mounts" ]] || {
echo "Target or one of its partitions is mounted:" >&2
printf '%s\n' "$target_mounts" >&2
exit 1
}
while IFS= read -r swap_name; do
[[ -z "$swap_name" ]] && continue
if [[ "$swap_name" == "$TARGET" || "$swap_name" == "${TARGET}p"* ]]; then
echo "Target contains active swap: $swap_name" >&2
exit 1
fi
done < <(swapon --noheadings --raw --show=NAME)
[[ -e /var/lib/rk1-media/provisioned ]] || {
echo "RK1 media provisioning has not completed" >&2
exit 1
}
[[ "$(dpkg-query -W -f='${Status}' rk1-media-stack 2>/dev/null)" == \
"install ok installed" ]] || {
echo "rk1-media-stack is not fully installed" >&2
exit 1
}
dpkg_audit=$(dpkg --audit)
[[ -z "$dpkg_audit" ]] || {
echo "dpkg reports an incomplete transaction:" >&2
printf '%s\n' "$dpkg_audit" >&2
exit 1
}
emmc_uuid=$(blkid -s UUID -o value "$root_source")
[[ -n "$emmc_uuid" ]] || { echo "Could not resolve eMMC UUID" >&2; exit 1; }
grep -Fxq "rootdev=UUID=$emmc_uuid" "$BOOT_ENV" || {
echo "$BOOT_ENV does not currently point at the running eMMC root" >&2
exit 1
}
if [[ -e "$BOOT_ENV_BACKUP" ]]; then
cmp --silent "$BOOT_ENV" "$BOOT_ENV_BACKUP" || {
echo "Existing boot-environment backup differs; refusing to overwrite it" >&2
exit 1
}
else
cp -a -- "$BOOT_ENV" "$BOOT_ENV_BACKUP"
fi
for apt_unit in apt-daily.service apt-daily-upgrade.service; do
apt_state=$(systemctl is-active "$apt_unit" 2>/dev/null || true)
case "$apt_state" in
inactive|failed|unknown) ;;
*) echo "Refusing migration while $apt_unit is $apt_state" >&2; exit 1 ;;
esac
done
provision_state=$(systemctl is-active rk1-media-provision.service 2>/dev/null || true)
case "$provision_state" in
inactive|failed|unknown|active) ;;
*) echo "Provisioning is not quiescent: $provision_state" >&2; exit 1 ;;
esac
echo "Confirmed source root: $root_source (UUID=$emmc_uuid)"
echo "ERASING target: $TARGET | $actual_model | serial $actual_serial"
apt_daily_timer_was_active=0
apt_upgrade_timer_was_active=0
unattended_was_active=0
kodi_was_active=0
systemctl is-active --quiet apt-daily.timer && apt_daily_timer_was_active=1
systemctl is-active --quiet apt-daily-upgrade.timer && apt_upgrade_timer_was_active=1
systemctl is-active --quiet unattended-upgrades.service && unattended_was_active=1
systemctl is-active --quiet kodi-rk.service && kodi_was_active=1
restore_services=0
restore_boot_env=0
cleanup() {
local rc=$?
trap - EXIT
if ((restore_boot_env)); then
echo "Migration did not validate; restoring the eMMC boot environment" >&2
if ! cp -a -- "$BOOT_ENV_BACKUP" "$BOOT_ENV" || \
! cmp --silent "$BOOT_ENV_BACKUP" "$BOOT_ENV"; then
echo "CRITICAL: could not restore and verify $BOOT_ENV" >&2
rc=1
fi
if ! sync; then
echo "CRITICAL: writeback failed while restoring $BOOT_ENV" >&2
rc=1
fi
fi
if ((restore_services)); then
((apt_daily_timer_was_active)) && systemctl start apt-daily.timer 2>/dev/null || true
((apt_upgrade_timer_was_active)) && systemctl start apt-daily-upgrade.timer 2>/dev/null || true
((unattended_was_active)) && systemctl start unattended-upgrades.service 2>/dev/null || true
((kodi_was_active)) && systemctl start kodi-rk.service 2>/dev/null || true
fi
exit "$rc"
}
trap cleanup EXIT
restore_services=1
systemctl stop apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
systemctl stop apt-daily.service apt-daily-upgrade.service 2>/dev/null || true
for apt_unit in apt-daily.service apt-daily-upgrade.service; do
apt_state=$(systemctl is-active "$apt_unit" 2>/dev/null || true)
case "$apt_state" in
inactive|failed|unknown) ;;
*) echo "Package service did not stop cleanly: $apt_unit is $apt_state" >&2; exit 1 ;;
esac
done
systemctl stop rk1-media-provision.service
! systemctl is-active --quiet rk1-media-provision.service || {
echo "Provisioning service did not stop cleanly" >&2
exit 1
}
systemctl stop unattended-upgrades.service 2>/dev/null || true
if systemctl list-unit-files kodi-rk.service --no-legend 2>/dev/null | grep -q '^kodi-rk\.service'; then
systemctl stop kodi-rk.service
! systemctl is-active --quiet kodi-rk.service || {
echo "Kodi did not stop cleanly" >&2
exit 1
}
fi
package_processes=$(ps -eo comm= | grep -E \
'^(apt|apt-get|dpkg|unattended-upgr)$' || true)
[[ -z "$package_processes" ]] || {
echo "Package-management process remains active:" >&2
printf '%s\n' "$package_processes" >&2
exit 1
}
lock_holders=$(lsof -t /var/lib/dpkg/lock /var/lib/dpkg/lock-frontend \
/var/cache/apt/archives/lock 2>/dev/null || true)
[[ -z "$lock_holders" ]] || {
echo "A package-management lock is still held by PID(s): $lock_holders" >&2
exit 1
}
dpkg_audit=$(dpkg --audit)
[[ -z "$dpkg_audit" ]] || {
echo "dpkg became inconsistent before migration:" >&2
printf '%s\n' "$dpkg_audit" >&2
exit 1
}
if [[ -s "$INSTALL_LOG" ]]; then
[[ ! -e "$INSTALL_LOG_BACKUP" ]] || {
echo "Installer log backup already exists: $INSTALL_LOG_BACKUP" >&2
exit 1
}
cp -a -- "$INSTALL_LOG" "$INSTALL_LOG_BACKUP"
fi
: >"$INSTALL_LOG"
chmod 0600 "$INSTALL_LOG"
sync
restore_boot_env=1
armbian-install --target "$TARGET" --boot sd --fs ext4 --yes
target_partition="${TARGET}p1"
udevadm settle
[[ -b "$target_partition" ]] || {
echo "Installer did not create $target_partition" >&2
exit 1
}
mapfile -t post_install_mounts < <(findmnt -rn -S "$target_partition" -o TARGET)
for mountpoint in "${post_install_mounts[@]}"; do
[[ "$mountpoint" == /mnt/armbian-install.* ]] || {
echo "Target remains mounted at unexpected path: $mountpoint" >&2
exit 1
}
umount "$mountpoint" || {
echo "Could not unmount installer path: $mountpoint" >&2
exit 1
}
done
findmnt -rn -S "$target_partition" >/dev/null && {
echo "Target partition remains mounted after installer teardown" >&2
exit 1
}
grep -Fq "scenario: sd install to $TARGET completed" "$INSTALL_LOG" || {
echo "Installer did not record successful sd-mode completion" >&2
exit 1
}
nvme_uuid=$(blkid -s UUID -o value "$target_partition")
[[ -n "$nvme_uuid" && "$nvme_uuid" != "$emmc_uuid" ]] || {
echo "NVMe UUID is missing or duplicates the eMMC UUID" >&2
exit 1
}
mapfile -t uuid_devices < <(blkid -t "UUID=$nvme_uuid" -o device)
[[ ${#uuid_devices[@]} -eq 1 && "${uuid_devices[0]}" == "$target_partition" ]] || {
echo "NVMe filesystem UUID is not unique" >&2
printf '%s\n' "${uuid_devices[@]}" >&2
exit 1
}
grep -Fxq "rootdev=UUID=$nvme_uuid" "$BOOT_ENV" || {
echo "eMMC boot environment does not point to the new NVMe root" >&2
exit 1
}
grep -Fxq 'rootfstype=ext4' "$BOOT_ENV" || {
echo "eMMC boot environment does not select ext4 for the new root" >&2
exit 1
}
e2fsck -fn "$target_partition"
target_fstab=$(debugfs -R 'cat /etc/fstab' "$target_partition" 2>/dev/null)
root_mount_count=$(grep -Ec '^[^#[:space:]][^[:space:]]*[[:space:]]+/[[:space:]]+' \
<<<"$target_fstab" || true)
[[ "$root_mount_count" -eq 1 ]] || {
echo "Target fstab does not contain exactly one root mount" >&2
exit 1
}
grep -Eq "^UUID=${nvme_uuid}[[:space:]]+/[[:space:]]+ext4" <<<"$target_fstab" || {
echo "Target fstab does not mount the NVMe UUID at /" >&2
exit 1
}
grep -Eq "^UUID=${emmc_uuid}[[:space:]]+/media/boot-media[[:space:]]+ext4" \
<<<"$target_fstab" || {
echo "Target fstab does not mount the eMMC boot medium" >&2
exit 1
}
grep -Eq '^/media/boot-media/boot[[:space:]]+/boot[[:space:]]+none[[:space:]]+bind' \
<<<"$target_fstab" || {
echo "Target fstab does not bind the eMMC /boot directory" >&2
exit 1
}
while read -r fstab_source _; do
case "$fstab_source" in
UUID=*)
fstab_uuid=${fstab_source#UUID=}
mapfile -t fstab_devices < <(blkid -t "UUID=$fstab_uuid" -o device)
[[ ${#fstab_devices[@]} -eq 1 ]] || {
echo "Target fstab UUID does not resolve uniquely: $fstab_uuid" >&2
exit 1
}
;;
PARTUUID=*)
fstab_partuuid=${fstab_source#PARTUUID=}
mapfile -t fstab_devices < <(blkid -t "PARTUUID=$fstab_partuuid" -o device)
[[ ${#fstab_devices[@]} -eq 1 ]] || {
echo "Target fstab PARTUUID does not resolve uniquely: $fstab_partuuid" >&2
exit 1
}
;;
esac
done <<<"$target_fstab"
sync
restore_boot_env=0
restore_services=0
echo "MIGRATION_VALIDATED"
echo "NVMe root: $target_partition (UUID=$nvme_uuid)"
echo "eMMC boot fallback: $BOOT_ENV_BACKUP"
echo "REBOOT_REQUIRED"