secubox-openwrt/package/secubox/luci-app-cloner/root/usr/sbin/secubox-asu-clone
CyberMind-FR b4fcccfbf9 feat(luci-app-cloner): Add partition tools to ASU builds
- Add fdisk, resize2fs, partx-utils to ASU package list
- Enables partition expansion on first boot for fresh installs
- Addresses kernel limitation with online ext4 resize

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 08:13:53 +01:00

510 lines
15 KiB
Bash

#!/bin/sh
#
# SecuBox ASU Clone Builder - On-the-fly firmware generation
# Uses ASU (Attended Sysupgrade) to build custom images with SecuBox provisioning
#
ASU_API="https://sysupgrade.openwrt.org/api/v1"
WORK_DIR="/tmp/asu-clone"
MASTER_KEY_FILE="/root/.ssh/id_dropbear.pub"
# Auto-detect master IP from br-lan
get_master_ip() {
ip -4 addr show br-lan 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1
}
MASTER_IP=$(get_master_ip)
SECUBOX_FEED="http://${MASTER_IP:-192.168.255.1}:8081/secubox-feed"
# Device profiles
get_asu_profile() {
case "$1" in
mochabin) echo "globalscale_mochabin" ;;
espressobin-v7) echo "globalscale_espressobin-v7" ;;
espressobin-ultra) echo "globalscale_espressobin-ultra" ;;
x86-64) echo "generic" ;;
*) echo "" ;;
esac
}
get_asu_target() {
case "$1" in
mochabin) echo "mvebu/cortexa72" ;;
espressobin*) echo "mvebu/cortexa53" ;;
x86-64) echo "x86/64" ;;
*) echo "" ;;
esac
}
# Request ASU build
request_build() {
local device="$1"
local version="${2:-24.10.5}"
local profile=$(get_asu_profile "$device")
local target=$(get_asu_target "$device")
[ -z "$profile" ] && { echo "Unknown device: $device"; return 1; }
echo "Requesting ASU build for $device ($profile) version $version..."
local resp=$(curl -s -X POST "$ASU_API/build" \
-H "Content-Type: application/json" \
-d "{
\"profile\": \"$profile\",
\"target\": \"$target\",
\"version\": \"$version\",
\"packages\": [\"luci\", \"luci-ssl\", \"luci-base\", \"luci-mod-admin-full\", \"luci-mod-network\", \"luci-mod-status\", \"luci-mod-system\", \"luci-proto-ipv6\", \"luci-theme-bootstrap\", \"uhttpd\", \"uhttpd-mod-ubus\", \"rpcd\", \"rpcd-mod-file\", \"rpcd-mod-iwinfo\", \"rpcd-mod-luci\", \"rpcd-mod-ucode\", \"wget-ssl\", \"curl\", \"kmod-usb-storage\", \"block-mount\", \"e2fsprogs\", \"fdisk\", \"resize2fs\", \"partx-utils\", \"dropbear\"]
}")
local hash=$(echo "$resp" | jsonfilter -e '@.request_hash' 2>/dev/null)
[ -z "$hash" ] && { echo "Failed to queue build"; echo "$resp"; return 1; }
echo "$hash"
}
# Wait for build completion
wait_build() {
local hash="$1"
local timeout="${2:-300}"
local elapsed=0
echo "Waiting for build $hash..."
while [ $elapsed -lt $timeout ]; do
sleep 10
elapsed=$((elapsed + 10))
local resp=$(curl -s "$ASU_API/build/$hash")
local status=$(echo "$resp" | jsonfilter -e '@.imagebuilder_status' 2>/dev/null)
echo " Status: $status ($elapsed/$timeout s)"
case "$status" in
done)
# Get ext4 image name
local img=$(echo "$resp" | grep -oE '"name":"[^"]*ext4-sdcard[^"]*"' | cut -d'"' -f4 | head -1)
[ -z "$img" ] && img=$(echo "$resp" | grep -oE '"name":"[^"]*ext4-combined[^"]*"' | cut -d'"' -f4 | head -1)
echo "https://sysupgrade.openwrt.org/store/$hash/$img"
return 0
;;
failed|error)
echo "Build failed!"
echo "$resp" | jsonfilter -e '@.detail' 2>/dev/null
return 1
;;
esac
done
echo "Build timeout"
return 1
}
# Download and customize image
customize_image() {
local img_url="$1"
local output="$2"
mkdir -p "$WORK_DIR"
cd "$WORK_DIR"
echo "Downloading image..."
wget -q "$img_url" -O image.img.gz || return 1
echo "Decompressing..."
gunzip -f image.img.gz || return 1
# Find root partition offset
local part2_start=$(fdisk -l image.img 2>/dev/null | grep "image.img2" | awk '{print $2}')
[ -z "$part2_start" ] && part2_start=36864
local offset=$((part2_start * 512))
echo "Mounting root partition (offset $offset)..."
mkdir -p mnt
mount -o loop,offset=$offset image.img mnt || return 1
# Add SSH key
if [ -f "$MASTER_KEY_FILE" ]; then
echo "Adding SSH key..."
mkdir -p mnt/etc/dropbear
cat "$MASTER_KEY_FILE" > mnt/etc/dropbear/authorized_keys
chmod 600 mnt/etc/dropbear/authorized_keys
fi
# Add master info
local master_ip=$(ip -4 addr show br-lan | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
local master_hostname=$(cat /proc/sys/kernel/hostname)
mkdir -p mnt/etc/secubox
echo "$master_hostname" > mnt/etc/secubox/master-hostname
echo "$master_ip" > mnt/etc/secubox/master-ip
# Add SecuBox provisioning script
cat > mnt/etc/uci-defaults/99-secubox-provision << 'PROVISION'
#!/bin/sh
# SecuBox Clone Auto-Provisioning
LOG="/tmp/secubox-provision.log"
echo "SecuBox provisioning started: $(date)" > $LOG
# Ensure firewall allows outbound traffic for opkg
/etc/init.d/firewall stop 2>/dev/null
echo "Firewall stopped for provisioning" >> $LOG
# Read master info
MASTER_IP=$(cat /etc/secubox/master-ip 2>/dev/null)
MASTER=$(cat /etc/secubox/master-hostname 2>/dev/null)
echo "Master: $MASTER @ $MASTER_IP" >> $LOG
# Wait for network with retry
wait_network() {
local tries=0
local max_tries=30
while [ $tries -lt $max_tries ]; do
if ping -c 1 -W 2 "$MASTER_IP" >/dev/null 2>&1; then
echo "Network ready after $tries attempts" >> $LOG
return 0
fi
tries=$((tries + 1))
sleep 5
done
echo "Network timeout after $max_tries attempts" >> $LOG
return 1
}
if ! wait_network; then
echo "ERROR: Cannot reach master, aborting provisioning" >> $LOG
# Re-enable firewall and exit
/etc/init.d/firewall start 2>/dev/null
exit 1
fi
# Add SecuBox feed
FEED_URL="http://${MASTER_IP}:8081/secubox-feed"
grep -q "secubox" /etc/opkg/customfeeds.conf 2>/dev/null || \
echo "src/gz secubox $FEED_URL" >> /etc/opkg/customfeeds.conf
echo "Added SecuBox feed: $FEED_URL" >> $LOG
# Disable signature verification for local feed
echo "option check_signature 0" >> /etc/opkg.conf
# Update package lists with retry
opkg_update_retry() {
local tries=0
local max_tries=5
while [ $tries -lt $max_tries ]; do
if opkg update >> $LOG 2>&1; then
echo "opkg update succeeded" >> $LOG
return 0
fi
tries=$((tries + 1))
echo "opkg update attempt $tries failed, retrying..." >> $LOG
sleep 10
done
return 1
}
if ! opkg_update_retry; then
echo "ERROR: opkg update failed after retries" >> $LOG
fi
# Install SecuBox core packages
PKGS="secubox-core luci-app-secubox luci-theme-secubox"
for pkg in $PKGS; do
echo "Installing $pkg..." >> $LOG
opkg install "$pkg" >> $LOG 2>&1 || echo "WARN: $pkg install failed" >> $LOG
done
# Optional: Install master-link if available
opkg install secubox-master-link >> $LOG 2>&1
# Join mesh if token provided
if [ -f /etc/secubox/clone-token ]; then
TOKEN=$(cat /etc/secubox/clone-token)
if [ -x /usr/lib/secubox/master-link.sh ]; then
/usr/lib/secubox/master-link.sh join "$MASTER_IP" "$TOKEN" >> $LOG 2>&1
fi
fi
# Re-enable firewall with proper rules
/etc/init.d/firewall start 2>/dev/null
# Ensure SSH stays accessible
uci set dropbear.@dropbear[0].Interface=''
uci commit dropbear
/etc/init.d/dropbear restart
echo "Provisioning complete: $(date)" >> $LOG
touch /etc/secubox/provisioned
exit 0
PROVISION
chmod +x mnt/etc/uci-defaults/99-secubox-provision
# Add partition expansion script (runs early on first boot)
cat > mnt/etc/uci-defaults/10-expand-rootfs << 'EXPAND'
#!/bin/sh
# Expand root partition to use full SD card/eMMC
# Handles UUID changes properly for boot compatibility
LOG="/tmp/expand-rootfs.log"
echo "Root expansion started: $(date)" > $LOG
# Detect root device
ROOT_DEV=""
if [ -b /dev/mmcblk0 ]; then
ROOT_DEV="/dev/mmcblk0"
ROOT_PART="${ROOT_DEV}p2"
BOOT_PART="${ROOT_DEV}p1"
elif [ -b /dev/sda ]; then
ROOT_DEV="/dev/sda"
ROOT_PART="${ROOT_DEV}2"
BOOT_PART="${ROOT_DEV}1"
else
echo "No suitable root device found" >> $LOG
exit 0
fi
echo "Root device: $ROOT_DEV, partition: $ROOT_PART" >> $LOG
# Check if already expanded (partition > 500MB = ~1M sectors)
PART_SIZE=$(cat /sys/class/block/$(basename $ROOT_PART)/size 2>/dev/null)
if [ -n "$PART_SIZE" ] && [ "$PART_SIZE" -gt 1000000 ]; then
echo "Partition already large ($PART_SIZE sectors), skipping expansion" >> $LOG
exit 0
fi
# Store current UUID before modification
OLD_UUID=$(blkid -s UUID -o value $ROOT_PART 2>/dev/null)
echo "Current root UUID: $OLD_UUID" >> $LOG
# Get current partition info
PART_START=$(fdisk -l $ROOT_DEV 2>/dev/null | grep "${ROOT_PART}" | awk '{print $2}')
[ -z "$PART_START" ] && { echo "Cannot detect partition start" >> $LOG; exit 0; }
echo "Partition 2 starts at sector $PART_START" >> $LOG
# Resize partition using fdisk (GPT-aware)
# Check if GPT or MBR
if fdisk -l $ROOT_DEV 2>/dev/null | grep -q "GPT"; then
echo "GPT partition table detected" >> $LOG
# Use sgdisk for GPT
if command -v sgdisk >/dev/null 2>&1; then
sgdisk -e $ROOT_DEV >> $LOG 2>&1 # Move backup GPT to end
sgdisk -d 2 $ROOT_DEV >> $LOG 2>&1 # Delete partition 2
sgdisk -n 2:$PART_START:0 $ROOT_DEV >> $LOG 2>&1 # New partition to end
sgdisk -t 2:8300 $ROOT_DEV >> $LOG 2>&1 # Set type to Linux filesystem
else
echo "sgdisk not available for GPT resize" >> $LOG
fi
else
echo "MBR partition table detected" >> $LOG
{
echo d # Delete partition
echo 2 # Partition 2
echo n # New partition
echo p # Primary
echo 2 # Partition 2
echo $PART_START # Same start
echo # Default end (full disk)
echo n # Don't remove ext4 signature
echo w # Write
} | fdisk $ROOT_DEV >> $LOG 2>&1
fi
echo "Partition table updated" >> $LOG
# Reread partition table
partprobe $ROOT_DEV 2>/dev/null || blockdev --rereadpt $ROOT_DEV 2>/dev/null
sleep 2
# Get new UUID (may have changed)
NEW_UUID=$(blkid -s UUID -o value $ROOT_PART 2>/dev/null)
echo "New root UUID: $NEW_UUID" >> $LOG
# Update fstab if UUID changed
if [ -n "$OLD_UUID" ] && [ -n "$NEW_UUID" ] && [ "$OLD_UUID" != "$NEW_UUID" ]; then
echo "UUID changed, updating fstab..." >> $LOG
sed -i "s/$OLD_UUID/$NEW_UUID/g" /etc/fstab 2>/dev/null
# Also update UCI fstab config
sed -i "s/$OLD_UUID/$NEW_UUID/g" /etc/config/fstab 2>/dev/null
fi
# Ensure boot partition is properly referenced (by device, not UUID for reliability)
# Update extlinux/grub if present
if [ -f /boot/extlinux/extlinux.conf ]; then
echo "Updating extlinux boot config..." >> $LOG
# Use PARTUUID or device path for reliability
PARTUUID=$(blkid -s PARTUUID -o value $ROOT_PART 2>/dev/null)
if [ -n "$PARTUUID" ]; then
sed -i "s/root=UUID=[^ ]*/root=PARTUUID=$PARTUUID/" /boot/extlinux/extlinux.conf 2>/dev/null
else
sed -i "s/root=UUID=[^ ]*/root=$ROOT_PART/" /boot/extlinux/extlinux.conf 2>/dev/null
fi
fi
# Create resize filesystem script to run after reboot
mkdir -p /etc/rc.local.d
cat > /etc/rc.local.d/resize-fs.sh << 'RESIZE_FS'
#!/bin/sh
# One-time filesystem resize after partition expansion
LOG="/tmp/resize-fs.log"
echo "Filesystem resize started: $(date)" > $LOG
sleep 5
ROOT_PART=""
if [ -b /dev/mmcblk0p2 ]; then
ROOT_PART="/dev/mmcblk0p2"
elif [ -b /dev/sda2 ]; then
ROOT_PART="/dev/sda2"
fi
if [ -n "$ROOT_PART" ]; then
echo "Resizing $ROOT_PART..." >> $LOG
# Check and resize ext4 filesystem
e2fsck -fy $ROOT_PART >> $LOG 2>&1
resize2fs $ROOT_PART >> $LOG 2>&1
echo "Resize complete" >> $LOG
# Show new size
df -h / >> $LOG 2>&1
fi
# Self-remove after execution
rm -f /etc/rc.local.d/resize-fs.sh
RESIZE_FS
chmod +x /etc/rc.local.d/resize-fs.sh
# Ensure rc.local runs scripts from rc.local.d
if [ -f /etc/rc.local ]; then
if ! grep -q "rc.local.d" /etc/rc.local; then
# Insert before exit 0
sed -i '/^exit 0/d' /etc/rc.local
cat >> /etc/rc.local << 'RCLOCAL'
# Run custom scripts
for script in /etc/rc.local.d/*.sh; do
[ -x "$script" ] && "$script"
done
exit 0
RCLOCAL
fi
else
cat > /etc/rc.local << 'RCLOCAL'
#!/bin/sh
# Run custom scripts
for script in /etc/rc.local.d/*.sh; do
[ -x "$script" ] && "$script"
done
exit 0
RCLOCAL
chmod +x /etc/rc.local
fi
echo "Expansion script complete, will resize filesystem after reboot" >> $LOG
echo "IMPORTANT: System should reboot to apply partition changes" >> $LOG
exit 0
EXPAND
chmod +x mnt/etc/uci-defaults/10-expand-rootfs
echo "Finalizing image..."
sync
umount mnt
gzip -c image.img > "$output"
# Cleanup
rm -rf "$WORK_DIR"
echo "Image ready: $output"
}
# Build and flash to remote
build_and_flash() {
local device="$1"
local remote_ip="$2"
local version="${3:-24.10.5}"
local token="$4"
echo "=== SecuBox ASU Clone Builder ==="
echo "Device: $device"
echo "Target: $remote_ip"
echo "Version: $version"
echo ""
# Request build
local hash=$(request_build "$device" "$version")
[ $? -ne 0 ] && return 1
# Wait for completion
local img_url=$(wait_build "$hash" 300)
[ $? -ne 0 ] && return 1
# Customize
local output="/srv/tftp/secubox-clone-${device}-asu.img.gz"
customize_image "$img_url" "$output" || return 1
# Inject token if provided
if [ -n "$token" ]; then
# Re-mount to add token
cd /tmp
gunzip -c "$output" > asu-tmp.img
local offset=$((36864 * 512))
mkdir -p mnt
mount -o loop,offset=$offset asu-tmp.img mnt
echo "$token" > mnt/etc/secubox/clone-token
sync
umount mnt
gzip -c asu-tmp.img > "$output"
rm -f asu-tmp.img
rmdir mnt
fi
# Also copy to web root
cp "$output" /www/secubox-clone-${device}-asu.img.gz
echo ""
echo "Image ready at:"
echo " - $output"
echo " - http://$(cat /etc/secubox/master-ip 2>/dev/null || echo '192.168.255.1')/secubox-clone-${device}-asu.img.gz"
# Flash if remote IP provided
if [ -n "$remote_ip" ]; then
echo ""
echo "Flashing to $remote_ip..."
cat "$output" | dbclient -i /root/.ssh/id_dropbear -y "root@$remote_ip" "cat > /tmp/firmware.img.gz && gunzip -f /tmp/firmware.img.gz" 2>/dev/null
dbclient -i /root/.ssh/id_dropbear -y "root@$remote_ip" "sysupgrade -n -F /tmp/firmware.img" 2>/dev/null &
echo "Flash initiated - device will reboot"
fi
return 0
}
# CLI
case "$1" in
build)
request_build "$2" "$3"
;;
wait)
wait_build "$2" "$3"
;;
customize)
customize_image "$2" "$3"
;;
flash)
# flash <device> <remote_ip> [version] [token]
build_and_flash "$2" "$3" "$4" "$5"
;;
*)
echo "SecuBox ASU Clone Builder"
echo ""
echo "Usage:"
echo " $0 build <device> [version] - Request ASU build"
echo " $0 wait <hash> [timeout] - Wait for build"
echo " $0 customize <url> <output> - Download and customize image"
echo " $0 flash <device> <ip> [ver] [token] - Full workflow"
echo ""
echo "Devices: mochabin, espressobin-v7, espressobin-ultra, x86-64"
echo "Default version: 24.10.5"
;;
esac