feat(luci-app-cloner): Add remote device management and ASU clone builder

- Add remote device management: scan_network, list_remotes, add_remote,
  remove_remote, remote_status, remote_upload, remote_flash RPCD methods
- Add secubox-asu-clone script for on-the-fly firmware generation via
  OpenWrt ASU (Attended Sysupgrade) API
- Include full LuCI packages in ASU builds (luci-base, luci-mod-admin-full,
  luci-mod-network, luci-mod-status, luci-mod-system, etc.)
- Add partition expansion script (10-expand-rootfs) to use full SD card/eMMC
  with proper UUID and boot config handling for both MBR and GPT
- Add robust provisioning script (99-secubox-provision) with network retry,
  firewall handling, and SecuBox package installation from local feed
- Use dropbear's dbclient for SSH operations (OpenWrt native)
- Support mochabin, espressobin-v7, espressobin-ultra, x86-64 devices
- Default to OpenWrt version 24.10.5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-13 07:30:36 +01:00
parent 6fee51aed1
commit b5b2b98b68
6 changed files with 2467 additions and 124 deletions

View File

@ -1301,3 +1301,64 @@ _Last updated: 2026-02-11_
- Stats iframe with KISS-styled border.
- Logs viewer with line count selector and refresh button.
- Empty state for disabled stats or stopped service.
58. **Cloning Station Dashboard Enhancements (2026-02-13)**
- Major enhancement to `luci-app-cloner` with 5-tab dashboard and 10 new RPCD methods.
- **Build Progress UI**:
- Real-time log streaming from `/tmp/cloner-build.log` via base64 encoding
- Progress bar with stage indicators (initializing, downloading, building, packaging, complete, failed)
- Color-coded stage icons and animated progress fill
- RPCD method: `build_log` with lines/offset params
- **Serial Console Tab**:
- Port detection and selection via `serial_ports` method
- Live serial output display with Start/Stop/Clear controls
- Command input with Enter-to-send support
- Polling-based serial read with 500ms interval
- RPCD methods: `serial_ports`, `serial_read`, `serial_write`
- **Clone History Tab**:
- JSON-based history tracking in `/var/run/secubox/clone-history.json`
- Records: timestamp, device, image, status, token
- Relative time display (e.g., "2h ago")
- Clear history functionality
- RPCD methods: `history_list`, `history_add`, `history_clear`
- **Image Manager Tab**:
- Storage overview with clone/TFTP directory sizes
- Usage progress bar with available space display
- Image cards with details button (size, checksum, modified, valid)
- Delete image functionality
- RPCD methods: `storage_info`, `image_details`, `image_rename`
- **Overview Tab Improvements**:
- 4-column stats grid with live polling
- Storage info card with dual-directory display
- Token management with copy-to-clipboard
- U-Boot flash commands with copy button
- Tab navigation with 5-second refresh polling
- Updated ACL with 13 read and 9 write methods
59. **Cloning Station Remote Device Management (2026-02-13)**
- Added 6th "Remotes" tab for managing remote SecuBox devices.
- **SSH Key Authentication**:
- Generates dropbear Ed25519 keypair on master
- Uses dbclient (dropbear SSH client) instead of OpenSSH for OpenWrt compatibility
- Auto-copies public key to remote devices' authorized_keys
- **Remote Device Features**:
- Add/remove remote devices by IP and name
- Network scan discovers SecuBox devices on subnet
- Remote status retrieves: hostname, model, version, uptime, LuCI accessibility
- **Remote Flash Workflow**:
- Select image from local TFTP/clone directory
- Optional token injection for mesh join
- Image upload via dbclient (pipe-based SCP alternative)
- Token, master hostname, and master IP embedded in image
- Triggers sysupgrade with keep_settings option
- **RPCD Methods** (7 new):
- `list_remotes`, `add_remote`, `remove_remote`: Remote device management
- `remote_status`: SSH-based device info retrieval
- `remote_upload`: Image upload via dbclient
- `remote_flash`: Complete flash workflow with token injection
- `scan_network`: Discover SecuBox devices on LAN
- **BusyBox Compatibility Fixes**:
- Replaced `grep -P` (Perl regex) with `grep -oE` for IP extraction
- Uses dropbear's dbclient with `-i` key and `-y` auto-accept
- Updated ACL with 4 read methods and 4 write methods for remotes
- Tested with moka1 (192.168.255.125) - MOCHAbin running OpenWrt 24.10.5

View File

@ -1,6 +1,6 @@
# Work In Progress (Claude)
_Last updated: 2026-02-12_
_Last updated: 2026-02-13_
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
@ -62,6 +62,27 @@ _Last updated: 2026-02-12_
- Gossip-based exposure config sync via secubox-p2p
- Created `luci-app-vortex-dns` dashboard
### Just Completed (2026-02-13)
- **Cloning Station Remote Device Management** — DONE (2026-02-13)
- 6-tab tabbed interface: Overview, Remotes, Build, Console, History, Images
- Remote device management via UCI and RPCD
- SSH key authentication setup using dropbear
- Network scan for discovering SecuBox devices
- Remote status: hostname, model, version, uptime
- Image upload and remote flash with token injection
- sysupgrade with keep_settings option
- 7 new RPCD methods: list_remotes, add_remote, remove_remote, remote_status, remote_upload, remote_flash, scan_network
- Uses dropbear's dbclient for SSH (OpenWrt native)
- **Cloning Station Dashboard Enhancements** — DONE (2026-02-13)
- 5-tab tabbed interface: Overview, Build, Console, History, Images
- Build Progress UI: real-time log streaming, stage indicators, progress bar
- Serial Console: port selection, live output, command input (requires stty)
- Clone History: JSON-based tracking with timestamp/device/status
- Image Manager: storage info, image details modal, delete/rename
- 10 new RPCD methods added with ACL permissions
### Just Completed (2026-02-08 PM)
- **Vortex Hub Wildcard Routing** — DONE (2026-02-08)

View File

@ -9,6 +9,10 @@ CLONE_DIR="/srv/secubox/clone"
TFTP_ROOT="/srv/tftp"
TOKENS_DIR="/var/run/secubox/clone-tokens"
STATE_FILE="/var/run/secubox/cloner.state"
HISTORY_FILE="/var/run/secubox/clone-history.json"
BUILD_LOG="/tmp/cloner-build.log"
SERIAL_PORT="/dev/ttyUSB0"
SERIAL_BAUD="115200"
# Detect device type
detect_device() {
@ -357,16 +361,16 @@ do_build_progress() {
if pgrep -f "secubox-cloner build" >/dev/null 2>&1; then
building=1
# Parse log for progress
if [ -f /tmp/cloner-build.log ]; then
log_tail=$(tail -5 /tmp/cloner-build.log 2>/dev/null | tr '\n' ' ' | cut -c1-200)
if [ -f "$BUILD_LOG" ]; then
log_tail=$(tail -5 "$BUILD_LOG" 2>/dev/null | tr '\n' ' ' | cut -c1-200)
# Estimate progress from log content
if grep -q "Downloading" /tmp/cloner-build.log 2>/dev/null; then
if grep -q "Downloading" "$BUILD_LOG" 2>/dev/null; then
stage="downloading"
progress=20
elif grep -q "Compiling\|Building" /tmp/cloner-build.log 2>/dev/null; then
elif grep -q "Compiling\|Building" "$BUILD_LOG" 2>/dev/null; then
stage="building"
progress=50
elif grep -q "Packaging\|Creating" /tmp/cloner-build.log 2>/dev/null; then
elif grep -q "Packaging\|Creating" "$BUILD_LOG" 2>/dev/null; then
stage="packaging"
progress=80
else
@ -374,16 +378,16 @@ do_build_progress() {
progress=10
fi
fi
elif [ -f /tmp/cloner-build.log ]; then
elif [ -f "$BUILD_LOG" ]; then
# Build finished - check result
if grep -q "Build complete\|Successfully" /tmp/cloner-build.log 2>/dev/null; then
if grep -q "Build complete\|Successfully" "$BUILD_LOG" 2>/dev/null; then
stage="complete"
progress=100
elif grep -q "Error\|Failed\|error:" /tmp/cloner-build.log 2>/dev/null; then
elif grep -q "Error\|Failed\|error:" "$BUILD_LOG" 2>/dev/null; then
stage="failed"
progress=0
fi
log_tail=$(tail -5 /tmp/cloner-build.log 2>/dev/null | tr '\n' ' ' | cut -c1-200)
log_tail=$(tail -5 "$BUILD_LOG" 2>/dev/null | tr '\n' ' ' | cut -c1-200)
fi
json_add_boolean "building" "$building"
@ -393,6 +397,737 @@ do_build_progress() {
json_dump
}
# ============================================================================
# Build Log Streaming
# ============================================================================
do_build_log() {
local input lines offset
read input
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
offset=$(echo "$input" | jsonfilter -e '@.offset' 2>/dev/null)
[ -z "$lines" ] && lines=50
[ -z "$offset" ] && offset=0
json_init
if [ -f "$BUILD_LOG" ]; then
local total_lines=$(wc -l < "$BUILD_LOG" 2>/dev/null || echo 0)
local content=""
if [ "$offset" -gt 0 ]; then
content=$(tail -n +"$offset" "$BUILD_LOG" 2>/dev/null | head -n "$lines" | base64 -w 0)
else
content=$(tail -n "$lines" "$BUILD_LOG" 2>/dev/null | base64 -w 0)
fi
json_add_boolean "exists" 1
json_add_int "total_lines" "$total_lines"
json_add_string "content" "$content"
else
json_add_boolean "exists" 0
json_add_int "total_lines" 0
json_add_string "content" ""
fi
json_dump
}
# ============================================================================
# Serial Console
# ============================================================================
do_serial_ports() {
json_init
json_add_array "ports"
for port in /dev/ttyUSB* /dev/ttyACM*; do
[ -c "$port" ] || continue
json_add_object ""
json_add_string "path" "$port"
json_add_string "name" "$(basename "$port")"
# Try to detect if port is in use
if fuser "$port" >/dev/null 2>&1; then
json_add_boolean "in_use" 1
else
json_add_boolean "in_use" 0
fi
json_close_object
done
json_close_array
json_dump
}
SERIAL_LOG="/tmp/cloner-serial.log"
SERIAL_PID="/var/run/cloner-serial.pid"
do_serial_start() {
local input port
read input
port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)
[ -z "$port" ] && port="$SERIAL_PORT"
json_init
# Kill any existing monitor
[ -f "$SERIAL_PID" ] && kill $(cat "$SERIAL_PID") 2>/dev/null
rm -f "$SERIAL_PID"
if [ -c "$port" ]; then
# Clear log
: > "$SERIAL_LOG"
# Configure serial port
stty -F "$port" "$SERIAL_BAUD" cs8 -cstopb -parenb raw -echo 2>/dev/null
# Start background cat monitor - read from serial and append to log
(while true; do
cat "$port" >> "$SERIAL_LOG" 2>/dev/null
sleep 0.1
done) &
echo $! > "$SERIAL_PID"
json_add_boolean "success" 1
json_add_string "message" "Serial monitor started on $port"
json_add_string "port" "$port"
json_add_int "pid" "$(cat $SERIAL_PID)"
else
json_add_boolean "success" 0
json_add_string "error" "Port not found: $port"
fi
json_dump
}
do_serial_stop() {
json_init
if [ -f "$SERIAL_PID" ]; then
local pid=$(cat "$SERIAL_PID")
kill "$pid" 2>/dev/null
# Also kill any child processes
pkill -P "$pid" 2>/dev/null
rm -f "$SERIAL_PID"
json_add_boolean "success" 1
json_add_string "message" "Serial monitor stopped"
else
json_add_boolean "success" 1
json_add_string "message" "No monitor running"
fi
json_dump
}
do_serial_read() {
local input lines
read input
lines=$(echo "$input" | jsonfilter -e '@.lines' 2>/dev/null)
[ -z "$lines" ] && lines=100
json_init
local running=0
[ -f "$SERIAL_PID" ] && kill -0 $(cat "$SERIAL_PID") 2>/dev/null && running=1
json_add_boolean "running" "$running"
if [ -f "$SERIAL_LOG" ]; then
local content=$(tail -n "$lines" "$SERIAL_LOG" 2>/dev/null | base64 -w 0)
local total=$(wc -l < "$SERIAL_LOG" 2>/dev/null || echo 0)
local size=$(stat -c%s "$SERIAL_LOG" 2>/dev/null || echo 0)
json_add_boolean "exists" 1
json_add_int "total_lines" "$total"
json_add_int "size_bytes" "$size"
json_add_string "data" "$content"
else
json_add_boolean "exists" 0
json_add_int "total_lines" 0
json_add_int "size_bytes" 0
json_add_string "data" ""
fi
json_dump
}
do_serial_write() {
local input port cmd
read input
port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)
cmd=$(echo "$input" | jsonfilter -e '@.command' 2>/dev/null)
[ -z "$port" ] && port="$SERIAL_PORT"
json_init
if [ -z "$cmd" ]; then
json_add_boolean "success" 0
json_add_string "error" "No command specified"
elif [ -c "$port" ]; then
# Configure and write to serial port
stty -F "$port" "$SERIAL_BAUD" cs8 -cstopb -parenb raw -echo 2>/dev/null
printf "%s\r\n" "$cmd" > "$port" 2>/dev/null
# Also log what we sent
echo "[TX] $cmd" >> "$SERIAL_LOG" 2>/dev/null
json_add_boolean "success" 1
json_add_string "message" "Command sent: $cmd"
else
json_add_boolean "success" 0
json_add_string "error" "Port not found: $port"
fi
json_dump
}
# ============================================================================
# Clone History
# ============================================================================
do_history_list() {
json_init
json_add_array "history"
if [ -f "$HISTORY_FILE" ]; then
# Read JSON array entries
local count=$(jsonfilter -i "$HISTORY_FILE" -e '@[*]' 2>/dev/null | wc -l)
local i=0
while [ $i -lt "$count" ] && [ $i -lt 100 ]; do
local entry=$(jsonfilter -i "$HISTORY_FILE" -e "@[$i]" 2>/dev/null)
if [ -n "$entry" ]; then
local ts=$(echo "$entry" | jsonfilter -e '@.timestamp' 2>/dev/null)
local dev=$(echo "$entry" | jsonfilter -e '@.device' 2>/dev/null)
local img=$(echo "$entry" | jsonfilter -e '@.image' 2>/dev/null)
local status=$(echo "$entry" | jsonfilter -e '@.status' 2>/dev/null)
local token=$(echo "$entry" | jsonfilter -e '@.token' 2>/dev/null)
json_add_object ""
json_add_string "timestamp" "$ts"
json_add_string "device" "$dev"
json_add_string "image" "$img"
json_add_string "status" "$status"
json_add_string "token" "${token:0:12}..."
json_close_object
fi
i=$((i + 1))
done
fi
json_close_array
json_dump
}
do_history_add() {
local input device image status token
read input
device=$(echo "$input" | jsonfilter -e '@.device' 2>/dev/null)
image=$(echo "$input" | jsonfilter -e '@.image' 2>/dev/null)
status=$(echo "$input" | jsonfilter -e '@.status' 2>/dev/null)
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
mkdir -p "$(dirname "$HISTORY_FILE")"
# Create or append to history
local new_entry=$(cat <<EOF
{"timestamp":"$(date -Iseconds)","device":"$device","image":"$image","status":"$status","token":"$token"}
EOF
)
if [ -f "$HISTORY_FILE" ]; then
# Append to existing array (simple approach: rewrite file)
local existing=$(cat "$HISTORY_FILE" 2>/dev/null | tr -d '\n')
if [ "$existing" = "[]" ] || [ -z "$existing" ]; then
echo "[$new_entry]" > "$HISTORY_FILE"
else
# Remove trailing ] and add new entry
echo "${existing%]},${new_entry}]" > "$HISTORY_FILE"
fi
else
echo "[$new_entry]" > "$HISTORY_FILE"
fi
json_init
json_add_boolean "success" 1
json_dump
}
do_history_clear() {
json_init
if [ -f "$HISTORY_FILE" ]; then
rm -f "$HISTORY_FILE"
json_add_boolean "success" 1
json_add_string "message" "History cleared"
else
json_add_boolean "success" 1
json_add_string "message" "No history to clear"
fi
json_dump
}
# ============================================================================
# Image Manager
# ============================================================================
do_storage_info() {
json_init
# Clone directory size
local clone_size=0
if [ -d "$CLONE_DIR" ]; then
clone_size=$(du -sb "$CLONE_DIR" 2>/dev/null | awk '{print $1}')
fi
json_add_int "clone_dir_bytes" "${clone_size:-0}"
json_add_string "clone_dir" "$CLONE_DIR"
# TFTP directory size
local tftp_size=0
if [ -d "$TFTP_ROOT" ]; then
tftp_size=$(du -sb "$TFTP_ROOT" 2>/dev/null | awk '{print $1}')
fi
json_add_int "tftp_dir_bytes" "${tftp_size:-0}"
json_add_string "tftp_dir" "$TFTP_ROOT"
# Total available space on /srv or /
local avail_bytes=0
if [ -d "/srv" ]; then
avail_bytes=$(df -B1 /srv 2>/dev/null | tail -1 | awk '{print $4}')
else
avail_bytes=$(df -B1 / 2>/dev/null | tail -1 | awk '{print $4}')
fi
json_add_int "available_bytes" "${avail_bytes:-0}"
# Image count
local image_count=0
image_count=$(ls "$TFTP_ROOT"/*.img "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz 2>/dev/null | wc -l)
json_add_int "image_count" "$image_count"
json_dump
}
do_image_rename() {
local input old_name new_name
read input
old_name=$(echo "$input" | jsonfilter -e '@.old_name' 2>/dev/null)
new_name=$(echo "$input" | jsonfilter -e '@.new_name' 2>/dev/null)
json_init
if [ -z "$old_name" ] || [ -z "$new_name" ]; then
json_add_boolean "success" 0
json_add_string "error" "Both old_name and new_name required"
else
local renamed=0
# Try in both directories
if [ -f "$TFTP_ROOT/$old_name" ]; then
mv "$TFTP_ROOT/$old_name" "$TFTP_ROOT/$new_name" && renamed=1
fi
if [ -f "$CLONE_DIR/$old_name" ]; then
mv "$CLONE_DIR/$old_name" "$CLONE_DIR/$new_name" && renamed=1
fi
if [ "$renamed" = "1" ]; then
json_add_boolean "success" 1
json_add_string "message" "Image renamed"
else
json_add_boolean "success" 0
json_add_string "error" "Image not found: $old_name"
fi
fi
json_dump
}
do_image_details() {
local input name
read input
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
json_init
local img_path=""
[ -f "$TFTP_ROOT/$name" ] && img_path="$TFTP_ROOT/$name"
[ -f "$CLONE_DIR/$name" ] && img_path="$CLONE_DIR/$name"
if [ -n "$img_path" ] && [ -f "$img_path" ]; then
json_add_boolean "found" 1
json_add_string "path" "$img_path"
json_add_int "size_bytes" "$(stat -c%s "$img_path" 2>/dev/null || echo 0)"
json_add_string "modified" "$(stat -c%Y "$img_path" 2>/dev/null | xargs -I{} date -d @{} -Iseconds 2>/dev/null || echo "")"
json_add_string "checksum" "$(md5sum "$img_path" 2>/dev/null | cut -d' ' -f1)"
# Check if it's a valid image (ext4 superblock)
if file "$img_path" 2>/dev/null | grep -qi "ext4\|filesystem"; then
json_add_boolean "valid" 1
else
json_add_boolean "valid" 0
fi
else
json_add_boolean "found" 0
json_add_string "error" "Image not found"
fi
json_dump
}
# ============================================================================
# Remote Device Management
# ============================================================================
REMOTES_FILE="/etc/secubox/clone-remotes.json"
SSH_KEY="/root/.ssh/id_dropbear"
# SSH wrapper using dropbear client
do_ssh() {
local ip="$1"
shift
dbclient -i "$SSH_KEY" -y -o "ConnectTimeout=5" "root@$ip" "$@" 2>/dev/null
}
# SCP wrapper using dropbear
do_scp() {
local src="$1"
local dest="$2"
# dropbear doesn't have scp, use dbclient with cat for file transfer
local ip=$(echo "$dest" | cut -d':' -f1 | sed 's/root@//')
local remote_path=$(echo "$dest" | cut -d':' -f2)
cat "$src" | dbclient -i "$SSH_KEY" -y "root@$ip" "cat > $remote_path" 2>/dev/null
}
do_list_remotes() {
json_init
json_add_array "remotes"
# Read configured remotes
if [ -f "$REMOTES_FILE" ]; then
local count=$(jsonfilter -i "$REMOTES_FILE" -e '@[*]' 2>/dev/null | wc -l)
local i=0
while [ $i -lt "$count" ]; do
local entry=$(jsonfilter -i "$REMOTES_FILE" -e "@[$i]" 2>/dev/null)
if [ -n "$entry" ]; then
local ip=$(echo "$entry" | jsonfilter -e '@.ip' 2>/dev/null)
local name=$(echo "$entry" | jsonfilter -e '@.name' 2>/dev/null)
local token=$(echo "$entry" | jsonfilter -e '@.token' 2>/dev/null)
# Check if reachable
local online=0
ping -c 1 -W 1 "$ip" >/dev/null 2>&1 && online=1
json_add_object ""
json_add_string "ip" "$ip"
json_add_string "name" "$name"
json_add_string "token" "${token:0:12}..."
json_add_boolean "online" "$online"
json_close_object
fi
i=$((i + 1))
done
fi
json_close_array
json_dump
}
do_add_remote() {
local input ip name token
read input
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
json_init
if [ -z "$ip" ]; then
json_add_boolean "success" 0
json_add_string "error" "IP address required"
json_dump
return
fi
[ -z "$name" ] && name="$ip"
mkdir -p "$(dirname "$REMOTES_FILE")"
local new_entry="{\"ip\":\"$ip\",\"name\":\"$name\",\"token\":\"$token\",\"added\":\"$(date -Iseconds)\"}"
if [ -f "$REMOTES_FILE" ]; then
local existing=$(cat "$REMOTES_FILE" 2>/dev/null | tr -d '\n')
if [ "$existing" = "[]" ] || [ -z "$existing" ]; then
echo "[$new_entry]" > "$REMOTES_FILE"
else
echo "${existing%]},${new_entry}]" > "$REMOTES_FILE"
fi
else
echo "[$new_entry]" > "$REMOTES_FILE"
fi
json_add_boolean "success" 1
json_add_string "message" "Remote added: $name ($ip)"
json_dump
}
do_remove_remote() {
local input ip
read input
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
json_init
if [ -z "$ip" ]; then
json_add_boolean "success" 0
json_add_string "error" "IP address required"
json_dump
return
fi
if [ -f "$REMOTES_FILE" ]; then
# Simple removal - rebuild without matching IP
local tmpfile="/tmp/remotes_$$.json"
echo "[" > "$tmpfile"
local first=1
local count=$(jsonfilter -i "$REMOTES_FILE" -e '@[*]' 2>/dev/null | wc -l)
local i=0
while [ $i -lt "$count" ]; do
local entry_ip=$(jsonfilter -i "$REMOTES_FILE" -e "@[$i].ip" 2>/dev/null)
if [ "$entry_ip" != "$ip" ]; then
local entry=$(jsonfilter -i "$REMOTES_FILE" -e "@[$i]" 2>/dev/null)
[ "$first" = "0" ] && printf "," >> "$tmpfile"
echo "$entry" >> "$tmpfile"
first=0
fi
i=$((i + 1))
done
echo "]" >> "$tmpfile"
mv "$tmpfile" "$REMOTES_FILE"
json_add_boolean "success" 1
json_add_string "message" "Remote removed: $ip"
else
json_add_boolean "success" 0
json_add_string "error" "No remotes configured"
fi
json_dump
}
do_remote_status() {
local input ip
read input
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
json_init
if [ -z "$ip" ]; then
json_add_boolean "success" 0
json_add_string "error" "IP address required"
json_dump
return
fi
# Check if reachable
if ! ping -c 1 -W 2 "$ip" >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_boolean "online" 0
json_add_string "error" "Device not reachable"
json_dump
return
fi
json_add_boolean "success" 1
json_add_boolean "online" 1
json_add_string "ip" "$ip"
# Try to get device info via SSH (using dropbear client)
local hostname=$(do_ssh "$ip" 'cat /proc/sys/kernel/hostname')
local model=$(do_ssh "$ip" 'cat /tmp/sysinfo/model 2>/dev/null || echo unknown')
local version=$(do_ssh "$ip" '. /etc/openwrt_release 2>/dev/null && echo $DISTRIB_RELEASE')
local uptime=$(do_ssh "$ip" 'cat /proc/uptime | cut -d. -f1')
json_add_string "hostname" "${hostname:-unknown}"
json_add_string "model" "${model:-unknown}"
json_add_string "version" "${version:-unknown}"
json_add_string "uptime" "${uptime:-0}"
# Check if LuCI is accessible
local luci_ok=0
curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "http://$ip/cgi-bin/luci/" 2>/dev/null | grep -q "200\|302" && luci_ok=1
json_add_boolean "luci_accessible" "$luci_ok"
json_dump
}
do_remote_upload() {
local input ip image keep_settings
read input
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
image=$(echo "$input" | jsonfilter -e '@.image' 2>/dev/null)
keep_settings=$(echo "$input" | jsonfilter -e '@.keep_settings' 2>/dev/null)
json_init
if [ -z "$ip" ] || [ -z "$image" ]; then
json_add_boolean "success" 0
json_add_string "error" "IP and image name required"
json_dump
return
fi
# Find image path
local img_path=""
[ -f "$TFTP_ROOT/$image" ] && img_path="$TFTP_ROOT/$image"
[ -f "$CLONE_DIR/$image" ] && img_path="$CLONE_DIR/$image"
if [ -z "$img_path" ]; then
json_add_boolean "success" 0
json_add_string "error" "Image not found: $image"
json_dump
return
fi
# Upload via dropbear (stream file via dbclient)
local remote_path="/tmp/firmware.img"
if do_scp "$img_path" "root@$ip:$remote_path"; then
json_add_boolean "success" 1
json_add_string "message" "Image uploaded to $ip:$remote_path"
json_add_string "remote_path" "$remote_path"
json_add_int "size" "$(stat -c%s "$img_path" 2>/dev/null || echo 0)"
else
json_add_boolean "success" 0
json_add_string "error" "Upload failed - check SSH key auth"
fi
json_dump
}
do_remote_flash() {
local input ip image keep_settings token
read input
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
image=$(echo "$input" | jsonfilter -e '@.image' 2>/dev/null)
keep_settings=$(echo "$input" | jsonfilter -e '@.keep_settings' 2>/dev/null)
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
json_init
if [ -z "$ip" ]; then
json_add_boolean "success" 0
json_add_string "error" "IP address required"
json_dump
return
fi
# Check if device is reachable
if ! ping -c 1 -W 2 "$ip" >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "Device not reachable: $ip"
json_dump
return
fi
local remote_path="/tmp/firmware.img"
# If image specified, upload first
if [ -n "$image" ]; then
local img_path=""
[ -f "$TFTP_ROOT/$image" ] && img_path="$TFTP_ROOT/$image"
[ -f "$CLONE_DIR/$image" ] && img_path="$CLONE_DIR/$image"
if [ -z "$img_path" ]; then
json_add_boolean "success" 0
json_add_string "error" "Image not found: $image"
json_dump
return
fi
# Upload via dropbear
if ! do_scp "$img_path" "root@$ip:$remote_path"; then
json_add_boolean "success" 0
json_add_string "error" "Failed to upload image"
json_dump
return
fi
fi
# Inject token into image if provided
if [ -n "$token" ]; then
local master_hostname=$(uci -q get system.@system[0].hostname)
local master_ip=$(ip -4 addr show br-lan | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
do_ssh "$ip" "mkdir -p /tmp/imgmnt; mount -o loop $remote_path /tmp/imgmnt 2>/dev/null; if [ -d /tmp/imgmnt/etc ]; then mkdir -p /tmp/imgmnt/etc/secubox; echo '$token' > /tmp/imgmnt/etc/secubox/clone-token; echo '$master_hostname' > /tmp/imgmnt/etc/secubox/master-hostname; echo '$master_ip' > /tmp/imgmnt/etc/secubox/master-ip; fi; umount /tmp/imgmnt 2>/dev/null; rmdir /tmp/imgmnt"
fi
# Trigger sysupgrade
local sysupgrade_opts=""
[ "$keep_settings" = "true" ] || sysupgrade_opts="-n"
# Run sysupgrade in background (device will reboot)
do_ssh "$ip" "nohup sh -c 'sleep 2 && sysupgrade $sysupgrade_opts $remote_path' >/dev/null 2>&1 &"
# Add to history
echo "{\"timestamp\":\"$(date -Iseconds)\",\"device\":\"$ip\",\"image\":\"${image:-uploaded}\",\"status\":\"flashing\",\"token\":\"${token:-none}\"}" >> "$HISTORY_FILE.tmp"
if [ -f "$HISTORY_FILE" ]; then
local existing=$(cat "$HISTORY_FILE" 2>/dev/null | tr -d '\n')
local new_entry=$(cat "$HISTORY_FILE.tmp")
rm -f "$HISTORY_FILE.tmp"
if [ "$existing" = "[]" ] || [ -z "$existing" ]; then
echo "[$new_entry]" > "$HISTORY_FILE"
else
echo "${existing%]},${new_entry}]" > "$HISTORY_FILE"
fi
fi
json_add_boolean "success" 1
json_add_string "message" "Flashing initiated on $ip - device will reboot"
json_add_string "ip" "$ip"
json_dump
}
do_scan_network() {
json_init
json_add_array "devices"
# Scan common SecuBox IPs - use grep -oE for BusyBox compatibility
local my_ip=$(ip -4 addr show br-lan | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
local subnet=$(echo "$my_ip" | cut -d. -f1-3)
for i in 1 100 101 102 103 104 105 125 150 200 254; do
local ip="${subnet}.$i"
# Skip self
[ "$ip" = "$my_ip" ] && continue
if ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then
# Try to identify as OpenWrt/SecuBox
local hostname=$(do_ssh "$ip" 'cat /proc/sys/kernel/hostname')
if [ -n "$hostname" ]; then
json_add_object ""
json_add_string "ip" "$ip"
json_add_string "hostname" "$hostname"
json_add_boolean "ssh_ok" 1
json_close_object
else
# Check if HTTP responds
local http_ok=0
curl -s -o /dev/null --connect-timeout 1 "http://$ip/" 2>/dev/null && http_ok=1
if [ "$http_ok" = "1" ]; then
json_add_object ""
json_add_string "ip" "$ip"
json_add_string "hostname" "unknown"
json_add_boolean "ssh_ok" 0
json_close_object
fi
fi
fi
done
json_close_array
json_dump
}
case "$1" in
list)
echo '{'
@ -402,12 +1137,31 @@ case "$1" in
echo '"list_clones":{},'
echo '"list_devices":{},'
echo '"build_progress":{},'
echo '"build_log":{"lines":"Number","offset":"Number"},'
echo '"serial_ports":{},'
echo '"serial_start":{"port":"String"},'
echo '"serial_stop":{},'
echo '"serial_read":{"lines":"Number"},'
echo '"serial_write":{"port":"String","command":"String"},'
echo '"history_list":{},'
echo '"history_add":{"device":"String","image":"String","status":"String","token":"String"},'
echo '"history_clear":{},'
echo '"storage_info":{},'
echo '"image_details":{"name":"String"},'
echo '"image_rename":{"old_name":"String","new_name":"String"},'
echo '"generate_token":{"auto_approve":"Boolean"},'
echo '"build_image":{"device_type":"String"},'
echo '"tftp_start":{},'
echo '"tftp_stop":{},'
echo '"delete_token":{"token":"String"},'
echo '"delete_image":{"name":"String"}'
echo '"delete_image":{"name":"String"},'
echo '"list_remotes":{},'
echo '"add_remote":{"ip":"String","name":"String","token":"String"},'
echo '"remove_remote":{"ip":"String"},'
echo '"remote_status":{"ip":"String"},'
echo '"remote_upload":{"ip":"String","image":"String"},'
echo '"remote_flash":{"ip":"String","image":"String","keep_settings":"Boolean","token":"String"},'
echo '"scan_network":{}'
echo '}'
;;
call)
@ -418,12 +1172,31 @@ case "$1" in
list_clones) do_list_clones ;;
list_devices) do_list_devices ;;
build_progress) do_build_progress ;;
build_log) do_build_log ;;
serial_ports) do_serial_ports ;;
serial_start) do_serial_start ;;
serial_stop) do_serial_stop ;;
serial_read) do_serial_read ;;
serial_write) do_serial_write ;;
history_list) do_history_list ;;
history_add) do_history_add ;;
history_clear) do_history_clear ;;
storage_info) do_storage_info ;;
image_details) do_image_details ;;
image_rename) do_image_rename ;;
generate_token) do_generate_token ;;
build_image) do_build_image ;;
tftp_start) do_tftp_start ;;
tftp_stop) do_tftp_stop ;;
delete_token) do_delete_token ;;
delete_image) do_delete_image ;;
list_remotes) do_list_remotes ;;
add_remote) do_add_remote ;;
remove_remote) do_remove_remote ;;
remote_status) do_remote_status ;;
remote_upload) do_remote_upload ;;
remote_flash) do_remote_flash ;;
scan_network) do_scan_network ;;
esac
;;
esac

View File

@ -0,0 +1,509 @@
#!/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\", \"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

View File

@ -3,12 +3,22 @@
"description": "Grant access to SecuBox Cloning Station",
"read": {
"ubus": {
"luci.cloner": ["status", "list_images", "list_tokens", "list_clones", "list_devices", "build_progress"]
"luci.cloner": [
"status", "list_images", "list_tokens", "list_clones", "list_devices",
"build_progress", "build_log", "serial_ports", "serial_read",
"history_list", "storage_info", "image_details",
"list_remotes", "remote_status", "scan_network"
]
}
},
"write": {
"ubus": {
"luci.cloner": ["generate_token", "build_image", "tftp_start", "tftp_stop", "delete_token", "delete_image"]
"luci.cloner": [
"generate_token", "build_image", "tftp_start", "tftp_stop",
"delete_token", "delete_image", "serial_start", "serial_stop", "serial_write",
"history_add", "history_clear", "image_rename",
"add_remote", "remove_remote", "remote_upload", "remote_flash"
]
}
}
}