Files
MikroTikPatch/setup-routeros.sh
elseif 433252aa17 modified: .github/workflows/patch_v7.yml
modified:   setup-routeros.sh
2026-09-02 11:24:23 +08:00

1170 lines
43 KiB
Bash

#!/bin/bash
BACKTITLE="MikroTik RouterOS LiveCD Installer"
TITLE="RouterOS Installation Wizard"
TITLE_SCAN="1/6 Scan Installation Media"
TITLE_SOURCE="2/6 Select Installation Source"
TITLE_PACKAGES="3/6 Select Packages"
TITLE_DISK="4/6 Select Target Disk"
TITLE_EXISTING="5/6 Existing Installation"
TITLE_CONFIRM="6/6 Confirm Installation"
TITLE_INSTALL="Installing RouterOS"
WORK_DIR="${WORK_DIR:-/tmp/ros_install}"
BUILD_DIR="$WORK_DIR/build"
BACKUP_DIR="$WORK_DIR/rw_backup"
BOOT_MNT="$WORK_DIR/mnt_boot"
ROS_MNT="$WORK_DIR/mnt_ros"
PROBE_MNT="$WORK_DIR/mnt_probe"
LOG_FILE="$WORK_DIR/install.log"
SOURCES_FILE="$WORK_DIR/sources.txt"
RC_FILE="$WORK_DIR/rc.txt"
ERR_FILE="$WORK_DIR/err.txt"
NPK_DIR=""
SOURCE_DIR=""
NPK_SOURCE_DIRS=()
MAIN_NPK=""
COMP_NPK_LIST=()
SELECTED_PACKAGES=()
TARGET_DISK=""
KEEP_CONFIG="no"
EXISTING_ROS_PART=""
LAST_ERROR=""
# Non-matching globs expand to nothing (zsh (N) qualifier equivalent)
shopt -s nullglob
# ------------------------------------------------------------------------------
# Helper functions: Logging & Cleanup
# ------------------------------------------------------------------------------
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
# All dialogs go through this wrapper: --erase-on-exit makes dialog wipe its
# own window on exit, so no leftovers garble the next dialog drawn over it.
dlg() {
dialog --erase-on-exit --backtitle "$BACKTITLE" "$@"
}
# Progress line for the programbox/progressbox consumers. Never fails the
# caller (ERR_EXIT friendly) and tolerates a closed pipe (Esc on the dialog).
step_msg() {
printf '%s\n' "$*" || true
}
cleanup_mounts() {
sync
umount "$BOOT_MNT" 2>/dev/null || true
umount "$ROS_MNT" 2>/dev/null || true
if [[ -d "$PROBE_MNT" ]]; then
local m
for m in "$PROBE_MNT"/*/; do
umount "$m" 2>/dev/null || true
done
fi
}
# ------------------------------------------------------------------------------
# Filter: Detect CD-ROM and USB devices
# ------------------------------------------------------------------------------
is_cdrom() {
local dev="$1"
[[ -b "$dev" ]] || return 1
local name
name=$(basename "$dev")
# 1. Standard optical drive names (/dev/sr*, /dev/cdrom*)
[[ "$name" =~ ^(sr|cdrom) ]] && return 0
# 2. Optical filesystem signatures (ISO9660, UDF)
local blkinfo
blkinfo=$(blkid "$dev" 2>/dev/null)
if [[ "$blkinfo" =~ (TYPE=\"iso9660\"|TYPE=\"udf\"|iso9660|udf) ]]; then
return 0
fi
# 3. SCSI device type 5 (CD-ROM/DVD)
local diskname="$name"
if [[ -f "/sys/class/block/$name/partition" ]]; then
diskname=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1)
[[ -z "$diskname" ]] && diskname="${name%%[0-9]*}"
fi
if [[ -f "/sys/block/$diskname/device/type" ]]; then
local dev_type
dev_type=$(cat "/sys/block/$diskname/device/type" 2>/dev/null)
[[ "$dev_type" == "5" ]] && return 0
fi
return 1
}
is_usb() {
local dev="$1"
[[ -b "$dev" ]] || return 1
local name
name=$(basename "$dev")
local diskname="$name"
if [[ -f "/sys/class/block/$name/partition" ]]; then
diskname=$(lsblk -no PKNAME "$dev" 2>/dev/null | head -n 1)
[[ -z "$diskname" ]] && diskname="${name%%[0-9]*}"
fi
# 1. sysfs device tree path containing /usb
local syspath
syspath=$(readlink -f "/sys/class/block/$diskname" 2>/dev/null || readlink -f "/sys/block/$diskname" 2>/dev/null)
if [[ "$syspath" == *"/usb"* ]]; then
return 0
fi
# 2. lsblk transport attribute
local tran
tran=$(lsblk -no TRAN "$dev" 2>/dev/null | head -n 1)
[[ "$tran" == "usb" ]] && return 0
# 3. udev property
if type udevadm >/dev/null 2>&1; then
local bus
bus=$(udevadm info -q property -n "$dev" 2>/dev/null | grep -E '^ID_BUS=' | cut -d= -f2)
[[ "$bus" == "usb" ]] && return 0
fi
return 1
}
is_cdrom_or_usb() {
is_cdrom "$1" || is_usb "$1"
}
# ------------------------------------------------------------------------------
# Check if a device or its symlink is already mounted
# ------------------------------------------------------------------------------
is_device_mounted() {
local dev="$1"
[[ -b "$dev" ]] || return 1
local realdev mntdev
realdev=$(readlink -f "$dev" 2>/dev/null || echo "$dev")
# Compare against the first field of each /proc/mounts line (the device)
while read -r mntdev _; do
if [[ "$mntdev" == "$realdev" || "$mntdev" == "$dev" ]]; then
return 0
fi
done < /proc/mounts
return 1
}
# ------------------------------------------------------------------------------
# Discover and Mount Installation Media
# ------------------------------------------------------------------------------
discover_and_mount_media() {
local dev realdev target_mnt b
mkdir -p /media/cdrom /media/usb "$PROBE_MNT"
# 1. Check optical drives (/dev/sr0, /dev/cdrom, etc.)
for dev in /dev/sr[0-9]* /dev/cdrom*; do
[[ -b "$dev" ]] || continue
is_device_mounted "$dev" && continue
realdev=$(readlink -f "$dev" 2>/dev/null || echo "$dev")
is_device_mounted "$realdev" && continue
target_mnt="/media/cdrom"
if grep -q " /media/cdrom " /proc/mounts 2>/dev/null; then
target_mnt="$PROBE_MNT/$(basename "$realdev")"
fi
mkdir -p "$target_mnt"
mount -t iso9660 -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \
mount -t udf -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || \
mount -o ro "$realdev" "$target_mnt" >/dev/null 2>&1 || true
done
# 2. Scan block devices from /sys/class/block/ (USB flash drives, etc.)
for b in /sys/class/block/*; do
dev="/dev/$(basename "$b")"
[[ -b "$dev" ]] || continue
# Optical drives (sr*/cdrom*) are owned by the loop above; never
# mount them onto the USB mount point. Name-based check only: an
# isohybrid USB stick (sd* with iso9660) must still be handled here.
[[ "$(basename "$dev")" =~ ^(sr|cdrom) ]] && continue
is_cdrom_or_usb "$dev" || continue
is_device_mounted "$dev" && continue
realdev=$(readlink -f "$dev" 2>/dev/null || echo "$dev")
is_device_mounted "$realdev" && continue
target_mnt="$PROBE_MNT/$(basename "$realdev")"
if ! grep -q " /media/usb " /proc/mounts 2>/dev/null; then
target_mnt="/media/usb"
fi
mkdir -p "$target_mnt"
mount -o ro "$dev" "$target_mnt" >/dev/null 2>&1 || \
mount -t iso9660 -o ro "$dev" "$target_mnt" >/dev/null 2>&1 || \
mount -t udf -o ro "$dev" "$target_mnt" >/dev/null 2>&1 || \
mount -t vfat -o ro "$dev" "$target_mnt" >/dev/null 2>&1 || \
mount -t ext4 -o ro "$dev" "$target_mnt" >/dev/null 2>&1 || \
mount -t ntfs -o ro "$dev" "$target_mnt" >/dev/null 2>&1 || true
done
}
get_media_dirs() {
local dev mnt rest d
local -a dirs
declare -A seen=()
# Inspect all currently mounted directories from /proc/mounts
while read -r dev mnt rest; do
[[ -n "$dev" ]] || continue
[[ -d "$mnt" ]] || continue
[[ "$mnt" == "/" || "$mnt" == "/proc"* || "$mnt" == "/sys"* || "$mnt" == "/dev"* ]] && continue
if is_cdrom_or_usb "$dev" || [[ "$mnt" =~ ^(/media|/cdrom|/mnt|/run/media|"$PROBE_MNT") ]]; then
[[ -z "${seen[$mnt]}" ]] && { seen[$mnt]=1; dirs+=("$mnt"); }
fi
done < /proc/mounts
# Emit one mount point per line
for d in "${dirs[@]}"; do
printf '%s\n' "$d"
done
}
# One canonical glob list for "where NPK files can live under a media dir".
# Every caller (scan, source menu, classification) uses the same patterns:
# the media dir itself, one level below it, and packages/ (depth 2).
npk_glob_results() {
local dir="$1"
[[ -d "$dir" ]] || return 0
find "$dir" -maxdepth 2 -type f -name '*.npk' 2>/dev/null | LC_ALL=C sort
}
# Return the routeros-*.npk system package of a media dir (empty if none).
find_main_npk_in() {
local dir="$1" npk_file
while IFS= read -r npk_file; do
[[ -z "$npk_file" ]] && continue
[[ "$(basename "$npk_file")" == routeros-*.npk ]] && { printf '%s\n' "$npk_file"; return 0; }
done < <(npk_glob_results "$dir")
return 0
}
# ------------------------------------------------------------------------------
# Classify NPK files found under one media source dir into the main system
# package (routeros-*.npk) and component packages (everything else).
# MAIN_NPK / COMP_NPK_LIST are set from the files of this source only
# ------------------------------------------------------------------------------
classify_npk_files() {
local npk_file bn tag
declare -A seen_tags=()
MAIN_NPK=""
COMP_NPK_LIST=()
for npk_file in "$@"; do
[[ -z "$npk_file" ]] && continue
bn=$(basename "$npk_file")
case "$bn" in
routeros-*.npk)
[[ -z "$MAIN_NPK" ]] && MAIN_NPK="$npk_file"
;;
*)
tag="${bn%.npk}"
tag="${tag%-[0-9]*}"
if [[ -z "${seen_tags[$tag]}" ]]; then
seen_tags[$tag]=1
COMP_NPK_LIST+=("$npk_file")
fi
;;
esac
done
}
# ------------------------------------------------------------------------------
# Step 1/6: Scan & Mount Media for NPK packages
# Runs inside a subshell piped into a dialog --progressbox. Writes one source
# dir per line into $SOURCES_FILE; exit code 0 = at least one valid source.
# ------------------------------------------------------------------------------
scan_media_npk() {
local m npk_file main_npk has_main
local -a media_dirs npk_files
declare -A seen_sources=()
: > "$SOURCES_FILE"
step_msg "Mounting CD-ROM and USB devices..."
discover_and_mount_media
log "Media discovery finished"
mapfile -t media_dirs < <(get_media_dirs)
log "Found media directories to scan: ${media_dirs[*]}"
if [[ ${#media_dirs[@]} -eq 0 ]]; then
step_msg "No CD-ROM or USB device detected."
return 1
fi
step_msg "Searching for RouterOS packages (*.npk)..."
for m in "${media_dirs[@]}"; do
step_msg "Scanning $m"
has_main=0
main_npk=""
mapfile -t npk_files < <(npk_glob_results "$m")
for npk_file in "${npk_files[@]}"; do
[[ -z "$npk_file" ]] && continue
log "Found NPK: $npk_file"
if [[ "$(basename "$npk_file")" == routeros-*.npk ]]; then
has_main=1
main_npk="$npk_file"
fi
done
# Only media containing a routeros-*.npk system package are valid sources
if [[ $has_main -eq 1 ]]; then
if [[ -z "${seen_sources[$m]}" ]]; then
seen_sources[$m]=1
printf '%s\n' "$m" >> "$SOURCES_FILE"
step_msg " -> valid source: $(basename "$main_npk")"
fi
fi
done
if [[ ! -s "$SOURCES_FILE" ]]; then
step_msg "No RouterOS system image (routeros-*.npk) found."
log "No media with routeros-*.npk found on scanned media"
return 1
fi
step_msg "Scan complete."
return 0
}
step_scan() {
while :; do
NPK_SOURCE_DIRS=()
: > "$RC_FILE"
( scan_media_npk; echo $? > "$RC_FILE" ) \
| dlg --title "$TITLE_SCAN" --progressbox "Scanning installation media..." 14 72
if [[ $(<"$RC_FILE") == 0 ]]; then
mapfile -t NPK_SOURCE_DIRS < "$SOURCES_FILE"
return 0
fi
# No valid source found on any media
dlg --title "$TITLE_SCAN" \
--yes-label "Retry" \
--no-label "Exit" \
--yesno "No RouterOS system image (routeros-*.npk) was found on CD-ROM or USB media.\n\nPlease insert the installation media and try again.\n\nSelect [Retry] to rescan, or [Exit] to cancel and return to the shell." 13 70
if [[ $? -eq 0 ]]; then
continue
else
return 255
fi
done
}
# ------------------------------------------------------------------------------
# Step 2/6: Select Installation Source
# Auto-selects a single source; shows a menu otherwise. Back returns to scan.
# ------------------------------------------------------------------------------
step_source() {
# Single source: auto-select without a dialog
if [[ ${#NPK_SOURCE_DIRS[@]} -eq 1 ]]; then
SOURCE_DIR="${NPK_SOURCE_DIRS[0]}"
log "Single installation source: $SOURCE_DIR"
return 0
fi
local -a menu_items
local m main_npk
for m in "${NPK_SOURCE_DIRS[@]}"; do
main_npk=$(find_main_npk_in "$m")
menu_items+=("$m" "$(basename "$main_npk")")
done
local choice
choice=$(dlg --stdout --title "$TITLE_SOURCE" \
--ok-label "Next" \
--cancel-label "Back" \
--extra-button --extra-label "Exit" \
--menu "Multiple installation sources were detected.\n\nSelect the media to install RouterOS from:" 16 74 6 \
"${menu_items[@]}")
local rc=$?
case $rc in
0)
SOURCE_DIR="$choice"
log "User selected installation source: $SOURCE_DIR"
return 0
;;
1)
return 1 # Back -> step 1 (rescan)
;;
3)
return 3 # Exit
;;
*)
return 255 # Exit (Esc)
;;
esac
}
# Is the given package tag in SELECTED_PACKAGES?
pkg_selected() {
local t
for t in "${SELECTED_PACKAGES[@]}"; do
[[ "$t" == "$1" ]] && return 0
done
return 1
}
# ------------------------------------------------------------------------------
# Step 3/6: Component Package Selection
# ------------------------------------------------------------------------------
step_packages() {
if [[ ${#COMP_NPK_LIST[@]} -eq 0 ]]; then
dlg --title "$TITLE_PACKAGES" \
--ok-label "OK" \
--extra-button --extra-label "Back" \
--msgbox "No optional packages were found on the installation media.\n\nOnly the base system will be installed.\n\nSystem package: $(basename "$MAIN_NPK")" 11 70
local rc=$?
case $rc in
0) return 0 ;; # OK
*) return 1 ;; # Back (extra button) or Esc
esac
fi
local -a check_items
declare -A added_tags=()
local npk_file bn tag is_on
for npk_file in "${COMP_NPK_LIST[@]}"; do
bn=$(basename "$npk_file")
tag="${bn%.npk}"
tag="${tag%-[0-9]*}"
# Strictly ensure unique tags
[[ -n "${added_tags[$tag]}" ]] && continue
added_tags[$tag]=1
is_on="ON"
if [[ ${#SELECTED_PACKAGES[@]} -gt 0 ]]; then
if pkg_selected "$tag"; then
is_on="ON"
else
is_on="OFF"
fi
fi
check_items+=("$tag" "$bn" "$is_on")
done
local choice
choice=$(dlg --stdout --title "$TITLE_PACKAGES" \
--separate-output \
--ok-label "Next" \
--cancel-label "Back" \
--extra-button --extra-label "Exit" \
--checklist "Select the optional component packages to install.\n\nUse SPACE to toggle an entry, TAB to move between the list and buttons." 18 72 8 \
"${check_items[@]}")
local rc=$?
log "step_packages returned code: $rc, choice: ${choice:-<empty>}"
case $rc in
0)
SELECTED_PACKAGES=()
if [[ -n "$choice" ]]; then
mapfile -t SELECTED_PACKAGES <<< "$choice"
fi
log "Selected packages: ${SELECTED_PACKAGES[*]}"
return 0
;;
1)
# Back to step 2
return 1
;;
3|255)
# Exit wizard
return 255
;;
esac
}
# ------------------------------------------------------------------------------
# Step 4/6: Target Disk Selection
# ------------------------------------------------------------------------------
step_disk() {
local -a menu_items
local dev size type model devname desc
# Scan all disk block devices: SATA, SAS, NVMe, VirtIO (vda), Xen (xvda), IDE (hda), eMMC (mmcblk)
while read -r dev size type model; do
[[ "$type" == "disk" ]] || continue
devname=$(basename "$dev")
# Exclude RAM, Loop, ZRAM, and optical CD-ROM devices
[[ "$devname" =~ ^(loop|ram|zram|sr) ]] && continue
# Exclude USB flash drives and USB installation media
if is_cdrom_or_usb "$dev"; then
continue
fi
# Construct friendly description
desc="${size}"
if [[ -n "$model" ]]; then
desc+=" ${model}"
else
if [[ "$devname" =~ ^vd ]]; then
desc+=" VirtIO-Disk"
elif [[ "$devname" =~ ^xvd ]]; then
desc+=" Xen-VDisk"
elif [[ "$devname" =~ ^nvme ]]; then
desc+=" NVMe-SSD"
elif [[ "$devname" =~ ^hd ]]; then
desc+=" IDE/ATA-Disk"
elif [[ "$devname" =~ ^sd ]]; then
desc+=" SATA/SCSI-Disk"
elif [[ "$devname" =~ ^mmcblk ]]; then
desc+=" eMMC-Storage"
else
desc+=" Hard-Disk"
fi
fi
menu_items+=("$dev" "$desc")
done < <(lsblk -d -p -n -o NAME,SIZE,TYPE,MODEL 2>/dev/null)
if [[ ${#menu_items[@]} -eq 0 ]]; then
dlg --title "$TITLE_DISK" \
--msgbox "No usable target hard disk was found.\n\nUSB drives and CD-ROM drives are excluded from the list of target disks." 11 62
return 1
fi
local choice
choice=$(dlg --stdout --title "$TITLE_DISK" \
--ok-label "Next" \
--cancel-label "Back" \
--extra-button --extra-label "Exit" \
--menu "Select the target disk for the RouterOS installation.\n\nWARNING: ALL EXISTING DATA ON THE SELECTED DISK WILL BE ERASED!" 18 74 7 \
"${menu_items[@]}")
local rc=$?
case $rc in
0)
TARGET_DISK="$choice"
log "Target disk selected: $TARGET_DISK"
return 0
;;
1)
return 1
;;
3|255)
return 255
;;
esac
}
# ------------------------------------------------------------------------------
# Step 5/6: Existing Installation & Config Retention
# ------------------------------------------------------------------------------
step_existing() {
EXISTING_ROS_PART=""
local candidate dev
# Preferred: the partition we label "RouterOS" ourselves; still verify /rw/store
candidate=$(blkid -L "RouterOS" 2>/dev/null | head -n 1)
mkdir -p "$ROS_MNT"
if [[ -n "$candidate" ]]; then
if mount -o ro "$candidate" "$ROS_MNT" 2>/dev/null; then
[[ -f "$ROS_MNT/rw/store" ]] && EXISTING_ROS_PART="$candidate"
umount "$ROS_MNT" 2>/dev/null || true
fi
fi
# Fallback: probe every block device for an existing RouterOS layout
if [[ -z "$EXISTING_ROS_PART" ]]; then
for dev in $(lsblk -p -o NAME -n 2>/dev/null | grep -E '^/dev/'); do
if mount -o ro "$dev" "$ROS_MNT" 2>/dev/null; then
if [[ -f "$ROS_MNT/rw/store" ]]; then
EXISTING_ROS_PART="$dev"
umount "$ROS_MNT" 2>/dev/null || true
break
fi
umount "$ROS_MNT" 2>/dev/null || true
fi
done
fi
if [[ -n "$EXISTING_ROS_PART" ]]; then
dlg --title "$TITLE_EXISTING" \
--yes-label "Keep Config" \
--no-label "Clean Install" \
--extra-button --extra-label "Back" \
--yesno "An existing RouterOS installation was detected on $EXISTING_ROS_PART.\n\nDo you want to keep the existing configuration?\n\nSelect [Keep Config] to back up /rw and restore it after installation, or [Clean Install] to discard it and start fresh." 15 70
local rc=$?
case $rc in
0)
KEEP_CONFIG="yes"
log "Keep config: yes (from $EXISTING_ROS_PART)"
return 0
;;
1)
KEEP_CONFIG="no"
log "Keep config: no"
return 0
;;
3)
# Back to step 4
return 1
;;
255)
return 255
;;
esac
fi
KEEP_CONFIG="no"
return 0
}
# ------------------------------------------------------------------------------
# Step 6/6: Installation Summary & Execution
# ------------------------------------------------------------------------------
step_install() {
# Detect boot mode early so it can be shown in the summary
local boot_mode="BIOS/Legacy"
if [[ -d /sys/firmware/efi ]]; then
boot_mode="UEFI"
fi
log "Boot mode: $boot_mode"
local pkgs_str="None (base system only)"
if [[ ${#SELECTED_PACKAGES[@]} -gt 0 ]]; then
pkgs_str=$(IFS=', '; echo "${SELECTED_PACKAGES[*]}")
fi
local summary="Please review the installation settings:\n\n"
summary+=" Source media : $NPK_DIR\n"
summary+=" System package : $(basename "$MAIN_NPK")\n"
summary+=" Components : $pkgs_str\n"
summary+=" Target disk : $TARGET_DISK\n"
summary+=" Boot mode : $boot_mode\n"
summary+=" Keep config : $KEEP_CONFIG\n\n"
summary+="WARNING: $TARGET_DISK will be partitioned and formatted.\nALL DATA ON THIS DISK WILL BE PERMANENTLY DESTROYED!"
dlg --title "$TITLE_CONFIRM" \
--yes-label "Install" \
--no-label "Back" \
--extra-button --extra-label "Exit" \
--yesno "$summary" 17 72
local rc=$?
case $rc in
0) ;; # Proceed with installation
1) return 1 ;; # Back to step 5
3|255) return 255 ;; # Exit
esac
# Start Installation
# Truncate the handshake files but keep appending to $LOG_FILE so the
# scan/source selections from earlier steps stay available for debugging.
: >> "$LOG_FILE"
log "--- Installation starting ---"
: > "$RC_FILE"
: > "$ERR_FILE"
# Execution Routine. Runs in a subshell piped into a dialog --programbox:
# progress lines go to stdout (the pipe), command output to the log file.
do_install_process() {
# If the user closes the dialog early (Esc), the pipe reader is gone;
# ignore SIGPIPE so the installation itself is not killed mid-way.
trap '' PIPE
# Note: no `set -e`/ERR trap here. Every critical command below is
# explicitly guarded with `if ! ... || { ...; return 1 }` and writes
# its own reason to $ERR_FILE; an ERR trap would fire on the guarded
# command itself and clobber the specific message with a generic one.
trap - ERR
set +e
# 1. Environment prep & config backup
step_msg "[ 5%] Step 1/8: Initializing environment..."
log "Step 1: Init environment"
cleanup_mounts
mkdir -p "$BUILD_DIR" "$BACKUP_DIR" "$BOOT_MNT" "$ROS_MNT"
rm -rf "$BUILD_DIR/sq"
if [[ "$KEEP_CONFIG" == "yes" && -n "$EXISTING_ROS_PART" ]]; then
step_msg "[ 10%] Step 1/8: Backing up existing configuration (/rw)..."
log "Backing up config from $EXISTING_ROS_PART"
mkdir -p "$ROS_MNT"
if mount -o ro "$EXISTING_ROS_PART" "$ROS_MNT" 2>/dev/null; then
rm -rf "$BACKUP_DIR/rw"
mkdir -p "$BACKUP_DIR/rw"
cp -a "$ROS_MNT/rw/." "$BACKUP_DIR/rw/" 2>/dev/null || true
umount "$ROS_MNT" 2>/dev/null || true
log "Config backup finished"
step_msg "[ 12%] Step 1/8: Environment ready & config backed up"
else
log "Warning: Could not mount $EXISTING_ROS_PART to backup /rw"
step_msg "[ 12%] Step 1/8: Environment ready (config backup skipped)"
fi
else
step_msg "[ 12%] Step 1/8: Environment & modules initialized"
fi
# 2. Extract NPK FILE_CONTAINER (bootloader, milo, EFI files)
step_msg "[ 18%] Step 2/8: Extracting NPK FILE_CONTAINER (boot files)..."
log "Step 2: Extracting NPK FILE_CONTAINER from $MAIN_NPK"
local sq="$BUILD_DIR/sq"
rm -rf "$sq" && mkdir -p "$sq"
local npkextract_bin
npkextract_bin=$(command -v npkextract 2>/dev/null || echo "")
if [[ -z "$npkextract_bin" ]]; then
step_msg "[ 18%] [ERR] Step 2/8: npkextract not found in PATH"
echo "npkextract is required but not found. Please ensure it is installed in the LiveCD." > "$ERR_FILE"
return 1
fi
log "Using npkextract: $npkextract_bin"
if ! "$npkextract_bin" "$MAIN_NPK" "$sq" >> "$LOG_FILE" 2>&1; then
step_msg "[ 18%] [ERR] Step 2/8: npkextract failed on $MAIN_NPK"
echo "npkextract failed to extract FILE_CONTAINER from $MAIN_NPK" > "$ERR_FILE"
return 1
fi
# Verify key boot files were extracted
if [[ ! -f "$sq/bin/milo" && ! -f "$sq/boot/EFI/BOOT/BOOTX64.EFI" ]]; then
step_msg "[ 18%] [ERR] Step 2/8: No boot files found after extraction"
echo "npkextract ran but neither milo nor BOOTX64.EFI was found in the NPK" > "$ERR_FILE"
return 1
fi
log "Extracted: milo=$(test -f "$sq/bin/milo" && echo yes || echo no), EFI=$(test -f "$sq/boot/EFI/BOOT/BOOTX64.EFI" && echo yes || echo no)"
step_msg "[ 25%] Step 2/8: Boot files extracted (milo + EFI ready)"
# Boot mode was detected in step_install (shown in summary); derive flag
local is_uefi=0
[[ "$boot_mode" == "UEFI" ]] && is_uefi=1
# 3. Disk partitioning
step_msg "[ 32%] Step 3/8: Partitioning $TARGET_DISK ..."
log "Step 3: Partitioning $TARGET_DISK (boot mode: $boot_mode)"
wipefs --all --force "$TARGET_DISK" >> "$LOG_FILE" 2>&1
local SECTORS=$(blockdev --getsz "$TARGET_DISK")
local LBA_END=$(( (SECTORS - 4096) / 8 * 8 - 1 ))
if ! sgdisk \
--set-alignment=1 \
--new=1:34:65535 --typecode=1:EF00 --change-name=1:"RouterOS Boot" \
--new=2:65536:$LBA_END --typecode=2:8300 --change-name=2:"RouterOS" \
--hybrid=1:2:EE \
"$TARGET_DISK" >> "$LOG_FILE" 2>&1; then
step_msg "[ 32%] [ERR] Step 3/8: sgdisk failed on $TARGET_DISK"
echo "sgdisk failed on $TARGET_DISK" > "$ERR_FILE"
return 1
fi
# Active boot partition
printf '\x80' | dd of="$TARGET_DISK" bs=1 seek=446 conv=notrunc >> "$LOG_FILE" 2>&1 || {
step_msg "[ 33%] [ERR] Step 3/8: Failed to Active boot partition on $TARGET_DISK"
echo "dd failed to Active boot partition on $TARGET_DISK" > "$ERR_FILE"
return 1
}
# Must be 0x83, otherwise "ERROR: could not find disk!"
printf '\x83' | dd of="$TARGET_DISK" bs=1 seek=450 conv=notrunc >> "$LOG_FILE" 2>&1 || {
step_msg "[ 33%] [ERR] Step 3/8: Failed to change partition type on $TARGET_DISK"
echo "dd failed to change partition type on $TARGET_DISK" > "$ERR_FILE"
return 1
}
if [[ $is_uefi -eq 0 ]]; then
# Write MikroTik BIOS bootstrap code into bytes 0-439 of the saved MBR
printf '%s' "FA31C08ED0BC007C89E65007501FFBFCBF0006B90001F2A5EA1D060000BEBE07B304803C807423803C00750983C610FECB75EFCD18BE9B06AC3C00740B56BB0700B40ECD105EEBF0EBFE8B148B4C0289F5BF0500BB007CB8010257CD135F730C31C0CD134F75EDBE7C06EBCCBFFE7D813D55AA75C289EEEA007C00004572726F72206C6F6164696E67206F7065726174696E6720737973746566D004D697373696E67206F7065726174696E672073797374656D0000000000" \
| xxd -r -p \
| dd of="$TARGET_DISK" bs=1 conv=notrunc >> "$LOG_FILE" 2>&1 || {
step_msg "[ 33%] [ERR] Step 3/8: Failed to write MBR bootstrap code"
echo "Failed to write MikroTik BIOS bootstrap into MBR" > "$ERR_FILE"
return 1
}
fi
log "MBR written to $TARGET_DISK"
# Force kernel to reread partition table & trigger mdev/udev
partprobe "$TARGET_DISK" >> "$LOG_FILE" 2>&1 || true
blockdev --rereadpt "$TARGET_DISK" 2>/dev/null || true
partx -u "$TARGET_DISK" 2>/dev/null || partx -a "$TARGET_DISK" 2>/dev/null || true
mdev -s 2>/dev/null || true
type udevadm >/dev/null 2>&1 && udevadm settle 2>/dev/null || true
# Calculate partition paths (nvme0n1p1, mmcblk0p1, vda1, sda1, etc.)
local bootp rosp
if [[ "$TARGET_DISK" =~ [0-9]$ ]]; then
bootp="${TARGET_DISK}p1"
rosp="${TARGET_DISK}p2"
else
bootp="${TARGET_DISK}1"
rosp="${TARGET_DISK}2"
fi
# Wait and ensure partition device nodes exist in /dev
local p wait_count pname maj_min
for p in "$bootp" "$rosp"; do
wait_count=0
# ~10s: partprobe/udev can take a while on large or slow disks
while [[ ! -b "$p" && $wait_count -lt 50 ]]; do
sleep 0.2
wait_count=$(( wait_count + 1 ))
mdev -s 2>/dev/null || true
pname=$(basename "$p")
if [[ -f "/sys/class/block/$pname/dev" ]]; then
maj_min=$(cat "/sys/class/block/$pname/dev")
mknod "$p" b "${maj_min%%:*}" "${maj_min#*:}" 2>/dev/null || true
fi
done
if [[ ! -b "$p" ]]; then
step_msg "[ 35%] [ERR] Step 3/8: Partition device node $p missing"
echo "Partition device node $p was not created by kernel" > "$ERR_FILE"
return 1
fi
done
step_msg "[ 45%] Step 3/8: Partitioned $TARGET_DISK (Boot 32M + System)"
# 4. Format partitions
if [[ $is_uefi -eq 0 ]]; then
step_msg "[ 48%] Step 4/8: Formatting $bootp (EXT2) & $rosp (EXT4)..."
log "Step 4: Formatting $bootp (ext2) and $rosp (ext4)"
if ! mkfs.ext2 -F -m 0 -b 4096 -L "RouterOS Boot" "$bootp" >> "$LOG_FILE" 2>&1; then
step_msg "[ 48%] [ERR] Step 4/8: Failed to format Boot partition ($bootp)"
echo "mkfs.ext2 failed to format Boot partition $bootp" > "$ERR_FILE"
return 1
fi
else
step_msg "[ 48%] Step 4/8: Formatting $bootp (FAT) & $rosp (EXT4)..."
log "Step 4: Formatting $bootp (vfat) and $rosp (ext4)"
if ! mkfs.vfat -F 16 -n "Boot" "$bootp" >> "$LOG_FILE" 2>&1; then
step_msg "[ 48%] [ERR] Step 4/8: Failed to format Boot partition ($bootp)"
echo "mkfs.vfat failed to format Boot partition $bootp" > "$ERR_FILE"
return 1
fi
fi
if ! mkfs.ext4 -F -m 0 -b 4096 -L "RouterOS" "$rosp" >> "$LOG_FILE" 2>&1; then
step_msg "[ 48%] [ERR] Step 4/8: Failed to format RouterOS partition ($rosp)"
echo "mkfs.ext4 failed to format RouterOS partition $rosp" > "$ERR_FILE"
return 1
fi
step_msg "[ 58%] Step 4/8: Formatted $bootp & $rosp"
# 5. Mount and prepare system dirs
step_msg "[ 62%] Step 5/8: Mounting partitions & creating system layout..."
log "Step 5: Mounting and creating dirs"
mkdir -p "$BOOT_MNT" "$ROS_MNT"
if ! mount "$bootp" "$BOOT_MNT" >> "$LOG_FILE" 2>&1; then
step_msg "[ 62%] [ERR] Step 5/8: Failed to mount Boot partition ($bootp)"
echo "Failed to mount Boot partition $bootp to $BOOT_MNT" > "$ERR_FILE"
return 1
fi
if ! mount -t ext4 "$rosp" "$ROS_MNT" >> "$LOG_FILE" 2>&1; then
step_msg "[ 62%] [ERR] Step 5/8: Failed to mount RouterOS partition ($rosp)"
echo "Failed to mount RouterOS partition $rosp to $ROS_MNT" > "$ERR_FILE"
return 1
fi
mkdir -p "$ROS_MNT/var/pdb/system" "$ROS_MNT/bin" "$ROS_MNT/boot"
step_msg "[ 70%] Step 5/8: Partitions mounted & directories created"
# 6. Install bootloader (EFI & Milo, using is_uefi detected above)
step_msg "[ 73%] Step 6/8: Installing bootloader..."
log "Step 6: Installing bootloader (boot mode: $boot_mode)"
# Install milo to $ROS_MNT/bin/milo (used by both BIOS and UEFI)
if [[ -f "$sq/bin/milo" ]]; then
cp -a "$sq/bin/." "$ROS_MNT/bin/" 2>> "$LOG_FILE" || {
step_msg "[ 73%] [ERR] Step 6/8: Failed to copy milo to $ROS_MNT/bin/"
echo "Failed to copy milo to $ROS_MNT/bin/milo" > "$ERR_FILE"
return 1
}
chmod 755 "$ROS_MNT/bin/milo"
log "Copied milo to $ROS_MNT/bin/milo"
else
log "Warning: milo not found in extracted NPK at $sq/bin/milo"
fi
# Copy the entire boot/ tree (EFI dir, boot map) to BOOT_MNT
if [[ -d "$sq/boot" ]]; then
rm -f "$sq/boot/map" 2>> "$LOG_FILE"
cp -a "$sq/boot/." "$BOOT_MNT/" 2>> "$LOG_FILE" || {
step_msg "[ 73%] [ERR] Step 6/8: Failed to copy boot/ to $BOOT_MNT"
echo "Failed to copy boot directory to $BOOT_MNT" > "$ERR_FILE"
return 1
}
log "Copied boot/ tree to $BOOT_MNT"
else
log "Warning: boot/ directory not found in extracted NPK at $sq/boot"
fi
if [[ $is_uefi -eq 0 ]]; then
# BIOS/Legacy boot: run milo to setup legacy boot sector on BOOT_MNT
if [[ -f "$ROS_MNT/bin/milo" ]]; then
log "BIOS mode: running milo $BOOT_MNT to install legacy boot sector"
"$ROS_MNT/bin/milo" "$BOOT_MNT" >> "$LOG_FILE" 2>&1 || {
log "Error: milo returned non-zero (may be non-fatal on some hardware)"
return 1
}
step_msg "[ 80%] Step 6/8: BIOS boot - Milo legacy boot sector installed"
else
log "Error: milo not available for BIOS boot setup"
step_msg "[ 80%] [ERR] Step 6/8: Failed to install legacy boot sector (milo not found)"
echo "milo not found: cannot install legacy boot sector" > "$ERR_FILE"
return 1
fi
else
# UEFI boot: EFI files already in place (BOOTX64.EFI was in boot/EFI/BOOT/)
log "UEFI mode: EFI bootloader in place at $BOOT_MNT/EFI/BOOT/BOOTX64.EFI - milo setup via EFI not needed"
step_msg "[ 80%] Step 6/8: UEFI boot - EFI bootloader installed"
fi
# 7. Copy system images and components
step_msg "[ 83%] Step 7/8: Writing $(basename "$MAIN_NPK") and packages..."
log "Step 7: Writing packages"
if ! cp "$MAIN_NPK" "$ROS_MNT/var/pdb/system/image" 2>> "$LOG_FILE" || [[ ! -s "$ROS_MNT/var/pdb/system/image" ]]; then
step_msg "[ 83%] [ERR] Step 7/8: Failed to write system image"
echo "Failed to copy main package to $ROS_MNT/var/pdb/system/image" > "$ERR_FILE"
return 1
fi
local p src bn comp_file
for p in "${SELECTED_PACKAGES[@]}"; do
[[ -z "$p" ]] && continue
src=""
for comp_file in "${COMP_NPK_LIST[@]}"; do
bn=$(basename "$comp_file")
if [[ "$bn" == ${p}-* || "$bn" == ${p}.npk ]]; then
src="$comp_file"
break
fi
done
if [[ -n "$src" && -f "$src" ]]; then
log "Installing component $p from $src"
mkdir -p "$ROS_MNT/var/pdb/$p"
cp "$src" "$ROS_MNT/var/pdb/$p/image" 2>> "$LOG_FILE" || log "Warning: Failed to copy $p"
else
log "Warning: component $p source not found"
fi
done
step_msg "[ 90%] Step 7/8: System image & selected components written"
# 8. Restore config & sync
step_msg "[ 93%] Step 8/8: Finalizing configuration & syncing disks..."
log "Step 8: Finalizing & sync"
mkdir -p "$ROS_MNT/rw/disk"
printf '#!/bin/sh\n# before RouterOS loader start\n' > "$ROS_MNT/rw/disk/rc.local"
chmod +x "$ROS_MNT/rw/disk/rc.local" 2>/dev/null || true
if [[ "$KEEP_CONFIG" == "yes" && -d "$BACKUP_DIR/rw" ]]; then
log "Restoring backup /rw configuration"
rm -rf "$ROS_MNT/rw"
cp -a "$BACKUP_DIR/rw" "$ROS_MNT/rw"
mkdir -p "$ROS_MNT/rw/disk"
[[ ! -f "$ROS_MNT/rw/disk/rc.local" ]] && printf '#!/bin/sh\n# before RouterOS loader start\n' > "$ROS_MNT/rw/disk/rc.local"
fi
sync
cleanup_mounts
step_msg "[100%] Step 8/8: Installation completed successfully!"
log "Installation succeeded!"
return 0
}
( do_install_process; echo $? > "$RC_FILE" ) \
| dlg --title "$TITLE_INSTALL" --programbox "Installing RouterOS - progress log:" 20 76
local install_rc
install_rc=$(<"$RC_FILE")
install_rc=${install_rc:-1}
if [[ -s "$ERR_FILE" ]]; then
LAST_ERROR=$(tail -n 1 "$ERR_FILE")
fi
if [[ "$install_rc" -eq 0 ]]; then
return 0
fi
LAST_ERROR="${LAST_ERROR:-An unexpected error occurred during installation. Please check install log.}"
cleanup_mounts
return 2
}
# ------------------------------------------------------------------------------
# Main wizard state machine
# ------------------------------------------------------------------------------
main() {
dlg --title "$TITLE" \
--yes-label "Yes" \
--no-label "No" \
--yesno "Welcome to the MikroTik RouterOS Installation Wizard.\n\nDo you want to start the installation now?\n\nSelect [Yes] to continue, or [No] to exit to the shell." 12 62
local start_rc=$?
if [[ $start_rc -ne 0 ]]; then
echo "Cancelled by user. Exiting wizard, returning to shell."
exit 0
fi
mkdir -p "$BUILD_DIR" "$BACKUP_DIR" "$BOOT_MNT" "$ROS_MNT" "$PROBE_MNT"
local step=1
local install_result=0
local rc
local -a src_files
local npk_file
while :; do
case $step in
1)
step_scan
rc=$?
if [[ $rc -eq 0 ]]; then
step=2
else
echo "Installation wizard cancelled."
exit 0
fi
;;
2)
step_source
rc=$?
if [[ $rc -eq 0 ]]; then
# Classify NPKs restricted to the chosen source
src_files=()
mapfile -t src_files < <(npk_glob_results "$SOURCE_DIR")
classify_npk_files "${src_files[@]}"
NPK_DIR=$(dirname "$MAIN_NPK")
log "Using source: $SOURCE_DIR, MAIN_NPK: $MAIN_NPK"
step=3
elif [[ $rc -eq 1 ]]; then
step=1 # Back -> rescan media
elif [[ $rc -eq 3 ]]; then
echo "Installation wizard cancelled."
exit 0
else
echo "Installation wizard cancelled."
exit 0
fi
;;
3)
step_packages
rc=$?
if [[ $rc -eq 0 ]]; then
step=4
elif [[ $rc -eq 1 ]]; then
# Back: to source selection, or straight to scan when the
# source was auto-selected (nothing to re-choose there)
if [[ ${#NPK_SOURCE_DIRS[@]} -gt 1 ]]; then
step=2
else
step=1
fi
else
echo "Installation wizard cancelled."
exit 0
fi
;;
4)
step_disk
rc=$?
if [[ $rc -eq 0 ]]; then
step=5
elif [[ $rc -eq 1 ]]; then
step=3
else
echo "Installation wizard cancelled."
exit 0
fi
;;
5)
step_existing
rc=$?
if [[ $rc -eq 0 ]]; then
step=6
elif [[ $rc -eq 1 ]]; then
step=4
else
echo "Installation wizard cancelled."
exit 0
fi
;;
6)
step_install
rc=$?
if [[ $rc -eq 0 ]]; then
install_result=0
break
elif [[ $rc -eq 1 ]]; then
step=4
elif [[ $rc -eq 255 ]]; then
echo "Installation wizard cancelled."
exit 0
else
# Error
install_result=1
break
fi
;;
esac
done
# --------------------------------------------------------------------------
# Finish & Reboot / Shell Prompt
# --------------------------------------------------------------------------
if [[ $install_result -eq 0 ]]; then
# Installation Success
dlg --title "Installation Complete" \
--yes-label "Reboot" \
--no-label "Shell" \
--yesno "RouterOS has been successfully installed to $TARGET_DISK.\n\nIMPORTANT: Remove the installation media (CD-ROM or USB drive) before rebooting.\n\nSelect [Reboot] to restart the system now, or [Shell] to exit to the shell." 14 70
local post_rc=$?
if [[ $post_rc -eq 0 ]]; then
echo "Rebooting system..."
reboot
else
echo "Installation completed. Returned to shell."
exit 0
fi
else
# Installation Failure
local err_detail="${LAST_ERROR}"
if [[ -f "$LOG_FILE" ]]; then
local tail_log
tail_log=$(tail -n 6 "$LOG_FILE" 2>/dev/null)
[[ -n "$tail_log" ]] && err_detail+="\n\nRecent Log Details:\n$tail_log"
fi
dlg --title "Installation Failed" \
--yes-label "Reboot" \
--no-label "Shell" \
--yesno "RouterOS installation failed.\n\nError details:\n$err_detail\n\nSelect [Reboot] to restart the system, or [Shell] to exit to the shell." 16 72
local post_rc=$?
if [[ $post_rc -eq 0 ]]; then
echo "Rebooting system..."
reboot
else
echo "Installation failed. Returned to shell. Log file: $LOG_FILE"
exit 1
fi
fi
}
main "$@"