#!/bin/sh # # RPCD handler for SecuBox Cloner # . /usr/share/libubox/jshn.sh 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() { local board_name="" [ -f /tmp/sysinfo/board_name ] && board_name=$(cat /tmp/sysinfo/board_name) [ -z "$board_name" ] && board_name=$(uci -q get system.@system[0].hostname 2>/dev/null) case "$board_name" in *mochabin*|*MOCHAbin*|globalscale,mochabin) echo "mochabin" ;; *espressobin*ultra*) echo "espressobin-ultra" ;; *espressobin*) echo "espressobin-v7" ;; *x86*|*generic*) echo "x86-64" ;; *) echo "unknown" ;; esac } get_lan_ip() { uci -q get network.lan.ipaddr 2>/dev/null || echo "192.168.255.1" } do_status() { local device_type lan_ip hostname tftp_enabled local has_image image_size image_name token_count clone_count json_init # Device info device_type=$(detect_device) lan_ip=$(get_lan_ip) hostname=$(uci -q get system.@system[0].hostname || echo "secubox") json_add_string "device_type" "$device_type" json_add_string "lan_ip" "$lan_ip" json_add_string "hostname" "$hostname" # TFTP status tftp_enabled=$(uci -q get dhcp.@dnsmasq[0].enable_tftp) json_add_boolean "tftp_running" "$([ "$tftp_enabled" = "1" ] && echo 1 || echo 0)" json_add_string "tftp_root" "$TFTP_ROOT" # Image status has_image=0 image_size="" image_name="" if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then has_image=1 image_size=$(ls -lh "$TFTP_ROOT/secubox-clone.img" 2>/dev/null | awk '{print $5}') image_name="secubox-clone.img" fi json_add_boolean "has_image" "$has_image" json_add_string "image_size" "${image_size:-0}" json_add_string "image_name" "${image_name:-}" # Token count token_count=0 [ -d "$TOKENS_DIR" ] && token_count=$(ls "$TOKENS_DIR"/*.json 2>/dev/null | wc -l) json_add_int "token_count" "$token_count" # Clone count (from master-link peer-list) clone_count=0 if [ -x /usr/lib/secubox/master-link.sh ]; then clone_count=$(/usr/lib/secubox/master-link.sh peer-list 2>/dev/null | grep -c "^[0-9]" || echo 0) fi json_add_int "clone_count" "$clone_count" # Build state if [ -f "$STATE_FILE" ]; then . "$STATE_FILE" json_add_string "last_build" "${BUILD_TIME:-}" else json_add_string "last_build" "" fi json_dump } do_list_images() { local img name device_type json_init json_add_array "images" # All TFTP-ready images for img in "$TFTP_ROOT"/secubox-clone*.img; do [ -f "$img" ] || continue name=$(basename "$img") # Extract device type from filename device_type=$(echo "$name" | sed -n 's/secubox-clone-\(.*\)\.img/\1/p') [ -z "$device_type" ] && device_type="mochabin" json_add_object "" json_add_string "name" "$name" json_add_string "path" "$img" json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')" json_add_string "device" "$device_type" json_add_boolean "tftp_ready" 1 json_close_object done # Clone directory images (not yet in TFTP) if [ -d "$CLONE_DIR" ]; then for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz; do [ -f "$img" ] || continue name=$(basename "$img") # Skip if already in TFTP [ -f "$TFTP_ROOT/${name%.gz}" ] && continue device_type=$(echo "$name" | sed -n 's/secubox-clone-\(.*\)\.img.*/\1/p') [ -z "$device_type" ] && device_type="unknown" json_add_object "" json_add_string "name" "$name" json_add_string "path" "$img" json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')" json_add_string "device" "$device_type" json_add_boolean "tftp_ready" 0 json_close_object done fi json_close_array json_dump } do_list_tokens() { local tf token created used auto json_init json_add_array "tokens" if [ -d "$TOKENS_DIR" ]; then for tf in "$TOKENS_DIR"/*.json; do [ -f "$tf" ] || continue token=$(jsonfilter -i "$tf" -e '@.token' 2>/dev/null) created=$(jsonfilter -i "$tf" -e '@.created' 2>/dev/null) used=$(jsonfilter -i "$tf" -e '@.used' 2>/dev/null) auto=$(jsonfilter -i "$tf" -e '@.auto_approve' 2>/dev/null) json_add_object "" json_add_string "token" "$token" json_add_string "token_short" "${token:0:16}..." json_add_string "created" "$created" json_add_boolean "used" "$([ "$used" = "true" ] && echo 1 || echo 0)" json_add_boolean "auto_approve" "$([ "$auto" = "true" ] && echo 1 || echo 0)" json_close_object done fi json_close_array json_dump } do_list_clones() { local peer_ip peer_name peer_status json_init json_add_array "clones" # Get peer list from WireGuard interfaces (most reliable source) # Each wg peer is a potential clone for wg in /etc/config/network; do # Get WireGuard peers from UCI uci -q show network 2>/dev/null | grep "\.public_key=" | while read -r line; do peer_name=$(echo "$line" | cut -d'.' -f2) # Skip if not a wireguard peer echo "$peer_name" | grep -q "^wg" || continue peer_ip=$(uci -q get "network.${peer_name}.endpoint_host" 2>/dev/null) [ -n "$peer_ip" ] || continue json_add_object "" json_add_string "info" "$peer_name ($peer_ip)" json_add_string "name" "$peer_name" json_add_string "ip" "$peer_ip" json_add_string "status" "active" json_close_object done break # Only need to run once done # Also check master-link peer-list if available if [ -x /usr/lib/secubox/master-link.sh ]; then /usr/lib/secubox/master-link.sh peer-list 2>/dev/null | grep "^[0-9]" > /tmp/cloner_peers.tmp 2>/dev/null while read -r line; do [ -n "$line" ] || continue json_add_object "" json_add_string "info" "$line" json_add_string "status" "mesh" json_close_object done < /tmp/cloner_peers.tmp 2>/dev/null rm -f /tmp/cloner_peers.tmp fi json_close_array json_dump } do_generate_token() { local input auto_approve token token_file read input auto_approve=$(echo "$input" | jsonfilter -e '@.auto_approve' 2>/dev/null) mkdir -p "$TOKENS_DIR" token=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1) token_file="$TOKENS_DIR/${token}.json" cat > "$token_file" </dev/null) version=$(echo "$input" | jsonfilter -e '@.version' 2>/dev/null) profile=$(echo "$input" | jsonfilter -e '@.profile' 2>/dev/null) # Defaults [ -z "$version" ] && version="24.10.5" [ -z "$profile" ] && profile="slim" json_init if [ -x /usr/sbin/secubox-cloner ]; then local cmd_args="" [ -n "$device_type" ] && cmd_args="$cmd_args --device $device_type" cmd_args="$cmd_args --version $version --profile $profile" (/usr/sbin/secubox-cloner build $cmd_args 2>&1 > /tmp/cloner-build.log) & json_add_boolean "success" 1 json_add_string "message" "Build started: ${device_type:-auto} / $version / $profile" json_add_string "device" "${device_type:-auto}" json_add_string "version" "$version" json_add_string "profile" "$profile" else json_add_boolean "success" 0 json_add_string "message" "secubox-cloner not installed" fi json_dump } do_list_devices() { json_init json_add_array "devices" json_add_object "" json_add_string "id" "mochabin" json_add_string "name" "Globalscale MOCHAbin" json_add_string "cpu" "Cortex-A72" json_close_object json_add_object "" json_add_string "id" "espressobin-v7" json_add_string "name" "Globalscale ESPRESSObin v7" json_add_string "cpu" "Cortex-A53" json_close_object json_add_object "" json_add_string "id" "espressobin-ultra" json_add_string "name" "Globalscale ESPRESSObin Ultra" json_add_string "cpu" "Cortex-A53" json_close_object json_add_object "" json_add_string "id" "x86-64" json_add_string "name" "Generic x86-64" json_add_string "cpu" "x86_64" json_close_object json_close_array json_dump } do_list_versions() { json_init json_add_array "versions" json_add_object "" json_add_string "id" "24.10.5" json_add_string "name" "OpenWrt 24.10.5" json_add_boolean "latest" 1 json_close_object json_add_object "" json_add_string "id" "24.10.0" json_add_string "name" "OpenWrt 24.10.0" json_add_boolean "latest" 0 json_close_object json_add_object "" json_add_string "id" "23.05.5" json_add_string "name" "OpenWrt 23.05.5 (LTS)" json_add_boolean "latest" 0 json_close_object json_add_object "" json_add_string "id" "23.05.4" json_add_string "name" "OpenWrt 23.05.4 (LTS)" json_add_boolean "latest" 0 json_close_object json_close_array json_add_string "default" "24.10.5" json_dump } do_list_build_profiles() { json_init json_add_array "profiles" json_add_object "" json_add_string "id" "slim" json_add_string "name" "Slim" json_add_string "description" "Minimal OpenWrt (LuCI + network essentials)" json_add_boolean "default" 1 json_close_object json_add_object "" json_add_string "id" "core" json_add_string "name" "Core" json_add_string "description" "Slim + SecuBox mesh (master-link, p2p)" json_add_boolean "default" 0 json_close_object json_add_object "" json_add_string "id" "full" json_add_string "name" "Full" json_add_string "description" "All SecuBox packages from current device" json_add_boolean "default" 0 json_close_object json_close_array json_dump } do_tftp_start() { json_init uci -q set dhcp.@dnsmasq[0].enable_tftp='1' uci -q set dhcp.@dnsmasq[0].tftp_root="$TFTP_ROOT" uci commit dhcp /etc/init.d/dnsmasq restart 2>/dev/null json_add_boolean "success" 1 json_add_string "message" "TFTP server started" json_dump } do_tftp_stop() { json_init uci -q set dhcp.@dnsmasq[0].enable_tftp='0' uci commit dhcp /etc/init.d/dnsmasq restart 2>/dev/null json_add_boolean "success" 1 json_add_string "message" "TFTP server stopped" json_dump } do_delete_token() { local input token read input token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null) json_init if [ -n "$token" ] && [ -f "$TOKENS_DIR/${token}.json" ]; then rm -f "$TOKENS_DIR/${token}.json" json_add_boolean "success" 1 json_add_string "message" "Token deleted" else json_add_boolean "success" 0 json_add_string "message" "Token not found" fi json_dump } do_delete_image() { local input name read input name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) json_init if [ -n "$name" ]; then rm -f "$CLONE_DIR/$name" "$TFTP_ROOT/$name" 2>/dev/null json_add_boolean "success" 1 json_add_string "message" "Image deleted" else json_add_boolean "success" 0 json_add_string "message" "Image not found" fi json_dump } do_build_progress() { local building=0 progress=0 stage="" log_tail="" json_init # Check if build is running if pgrep -f "secubox-cloner build" >/dev/null 2>&1; then building=1 # Parse log for progress 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" "$BUILD_LOG" 2>/dev/null; then stage="downloading" progress=20 elif grep -q "Compiling\|Building" "$BUILD_LOG" 2>/dev/null; then stage="building" progress=50 elif grep -q "Packaging\|Creating" "$BUILD_LOG" 2>/dev/null; then stage="packaging" progress=80 else stage="initializing" progress=10 fi fi elif [ -f "$BUILD_LOG" ]; then # Build finished - check result if grep -q "Build complete\|Successfully" "$BUILD_LOG" 2>/dev/null; then stage="complete" progress=100 elif grep -q "Error\|Failed\|error:" "$BUILD_LOG" 2>/dev/null; then stage="failed" progress=0 fi log_tail=$(tail -5 "$BUILD_LOG" 2>/dev/null | tr '\n' ' ' | cut -c1-200) fi json_add_boolean "building" "$building" json_add_int "progress" "$progress" json_add_string "stage" "$stage" json_add_string "log" "$log_tail" 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 </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) # Check source file exists [ ! -f "$src" ] && return 1 # Clean any stale host keys (prevents mismatch errors after device reflash) for khfile in /root/.ssh/known_hosts /.ssh/known_hosts /overlay/upper/.ssh/known_hosts; do [ -f "$khfile" ] && sed -i "/^$ip /d" "$khfile" 2>/dev/null done # Transfer file - dbclient with -y accepts new host keys cat "$src" | dbclient -i "$SSH_KEY" -y "root@$ip" "cat > $remote_path" 2>/dev/null local ret=$? # Verify file exists on remote (BusyBox compatible) if [ $ret -eq 0 ]; then dbclient -i "$SSH_KEY" -y "root@$ip" "[ -s $remote_path ]" 2>/dev/null && return 0 fi return 1 } 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 } # ============================================================================ # Staged Flash Methods (KISS Remote Flash v2) # ============================================================================ do_remote_prepare_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 if [ -z "$image" ]; then json_add_boolean "success" 0 json_add_string "error" "Image required" json_dump return fi 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 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 local img_size=$(ls -lh "$img_path" | awk '{print $5}') if ! do_scp "$img_path" "root@$ip:/tmp/firmware.img"; then json_add_boolean "success" 0 json_add_string "error" "Failed to upload image" json_dump return fi local sysupgrade_opts="-n" [ "$keep_settings" = "true" ] && sysupgrade_opts="" do_ssh "$ip" "cat > /etc/secubox-staged-flash.sh << 'FLASHEOF' #!/bin/sh if [ -f /tmp/firmware.img ] && [ -f /root/.secubox-flash-opts ]; then rm -f /root/.secubox-flash-opts sysupgrade $sysupgrade_opts /tmp/firmware.img fi FLASHEOF chmod +x /etc/secubox-staged-flash.sh" do_ssh "$ip" "grep -q 'secubox-staged-flash' /etc/rc.local 2>/dev/null || sed -i '/^exit 0/i /etc/secubox-staged-flash.sh' /etc/rc.local" do_ssh "$ip" "touch /root/.secubox-flash-opts" json_add_boolean "success" 1 json_add_string "message" "Flash prepared - awaiting confirmation" json_add_string "ip" "$ip" json_add_string "image" "$image" json_add_string "size" "$img_size" json_dump } do_remote_confirm_flash() { 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 firmware exists (try both /tmp and /root) local fw_path=$(do_ssh "$ip" "[ -f /tmp/firmware.img ] && echo /tmp/firmware.img || ([ -f /tmp/firmware.img ] && echo /tmp/firmware.img)" 2>/dev/null) if [ -z "$fw_path" ]; then json_add_boolean "success" 0 json_add_string "error" "No firmware found on $ip" json_dump return fi # Get sysupgrade options local opts=$(do_ssh "$ip" "cat /root/.secubox-flash-opts 2>/dev/null || echo -n" 2>/dev/null) # Execute sysupgrade directly - SSH will disconnect when device reboots do_ssh "$ip" "sysupgrade $opts $fw_path" 2>/dev/null & json_add_boolean "success" 1 json_add_string "message" "Sysupgrade started on $ip" json_add_string "ip" "$ip" json_add_string "firmware" "$fw_path" json_dump } do_remote_flash_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 if ! ping -c 1 -W 2 "$ip" >/dev/null 2>&1; then json_add_boolean "success" 1 json_add_string "status" "offline" json_add_string "ip" "$ip" json_dump return fi local has_fw=$(do_ssh "$ip" "[ -f /tmp/firmware.img ] && echo yes || echo no" 2>/dev/null) local has_marker=$(do_ssh "$ip" "[ -f /root/.secubox-flash-opts ] && echo yes || echo no" 2>/dev/null) local status="none" [ "$has_fw" = "yes" ] && [ "$has_marker" = "yes" ] && status="pending" [ "$has_fw" = "yes" ] && [ "$has_marker" = "no" ] && status="ready" json_add_boolean "success" 1 json_add_string "status" "$status" json_add_string "ip" "$ip" json_add_string "has_firmware" "$has_fw" json_add_string "has_marker" "$has_marker" json_dump } do_remote_cancel_flash() { 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 do_ssh "$ip" "rm -f /tmp/firmware.img /root/.secubox-flash-opts /etc/secubox-staged-flash.sh; sed -i '/secubox-staged-flash/d' /etc/rc.local" 2>/dev/null json_add_boolean "success" 1 json_add_string "message" "Flash cancelled" 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 } # ============================================================================ # Factory Auto-Provisioning Methods # ============================================================================ do_pending_devices() { local pending_json="[]" if [ -f /usr/lib/secubox/master-link.sh ]; then . /usr/lib/secubox/master-link.sh 2>/dev/null pending_json=$(ml_discovery_pending 2>/dev/null) [ -z "$pending_json" ] && pending_json="[]" fi # Wrap array in object for ubus printf '{"devices":%s}\n' "$pending_json" } do_approve_device() { local input device_id profile result read input device_id=$(echo "$input" | jsonfilter -e '@.device_id' 2>/dev/null) profile=$(echo "$input" | jsonfilter -e '@.profile' 2>/dev/null) if [ -z "$device_id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing device_id" json_dump return fi if [ -f /usr/lib/secubox/master-link.sh ]; then . /usr/lib/secubox/master-link.sh 2>/dev/null result=$(ml_discovery_approve "$device_id" "$profile" 2>/dev/null) # Result is already JSON object [ -n "$result" ] && echo "$result" || echo '{"success":false,"error":"approval failed"}' return fi json_init json_add_boolean "success" 0 json_add_string "error" "master-link not available" json_dump } do_reject_device() { local input device_id reason result read input device_id=$(echo "$input" | jsonfilter -e '@.device_id' 2>/dev/null) reason=$(echo "$input" | jsonfilter -e '@.reason' 2>/dev/null) if [ -z "$device_id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing device_id" json_dump return fi if [ -f /usr/lib/secubox/master-link.sh ]; then . /usr/lib/secubox/master-link.sh 2>/dev/null result=$(ml_discovery_reject "$device_id" "$reason" 2>/dev/null) # Result is already JSON object [ -n "$result" ] && echo "$result" || echo '{"success":false,"error":"rejection failed"}' return fi json_init json_add_boolean "success" 0 json_add_string "error" "master-link not available" json_dump } do_bulk_tokens() { local input count profile ttl tokens_json read input count=$(echo "$input" | jsonfilter -e '@.count' 2>/dev/null) profile=$(echo "$input" | jsonfilter -e '@.profile' 2>/dev/null) ttl=$(echo "$input" | jsonfilter -e '@.ttl' 2>/dev/null) [ -z "$count" ] && count=10 [ -z "$profile" ] && profile="default" [ -z "$ttl" ] && ttl=86400 if [ -f /usr/lib/secubox/master-link.sh ]; then . /usr/lib/secubox/master-link.sh 2>/dev/null tokens_json=$(ml_bulk_tokens "$count" "$profile" "$ttl" 2>/dev/null) [ -z "$tokens_json" ] && tokens_json="[]" printf '{"tokens":%s}\n' "$tokens_json" return fi json_init json_add_boolean "success" 0 json_add_string "error" "master-link not available" json_dump } do_inventory() { local inv_json="[]" if [ -f /usr/lib/secubox/inventory.sh ]; then . /usr/lib/secubox/inventory.sh 2>/dev/null inv_json=$(inventory_list 2>/dev/null) [ -z "$inv_json" ] && inv_json="[]" fi # Wrap array in object for ubus printf '{"inventory":%s}\n' "$inv_json" } do_list_profiles() { local prof_json="[]" if [ -f /usr/lib/secubox/profiles.sh ]; then . /usr/lib/secubox/profiles.sh 2>/dev/null prof_json=$(profile_list 2>/dev/null) [ -z "$prof_json" ] && prof_json="[]" fi # Wrap array in object for ubus printf '{"profiles":%s}\n' "$prof_json" } do_import_preregistered() { local input devices read input devices=$(echo "$input" | jsonfilter -e '@.devices' 2>/dev/null) json_init if [ -z "$devices" ]; then json_add_boolean "success" 0 json_add_string "error" "Missing devices data" json_dump return fi local preregistered="/etc/secubox/preregistered.json" mkdir -p "$(dirname "$preregistered")" # Write devices to pre-registered file echo "$devices" > "$preregistered" local count=$(echo "$devices" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) json_add_boolean "success" 1 json_add_int "imported" "$count" json_dump } do_discovery_status() { json_init local discovery_mode=$(uci -q get master-link.main.discovery_mode) local auto_approve_known=$(uci -q get master-link.main.auto_approve_known) local default_profile=$(uci -q get master-link.main.default_profile) json_add_boolean "discovery_enabled" "$([ "$discovery_mode" = "1" ] && echo 1 || echo 0)" json_add_boolean "auto_approve_known" "$([ "$auto_approve_known" = "1" ] && echo 1 || echo 0)" json_add_string "default_profile" "${default_profile:-default}" # Count pending devices local pending_count=0 if [ -d /var/lib/secubox-master-link/pending ]; then pending_count=$(ls -1 /var/lib/secubox-master-link/pending/*.json 2>/dev/null | wc -l) fi json_add_int "pending_count" "$pending_count" # Count inventoried devices local inventory_count=0 if [ -d /var/lib/secubox-factory/inventory ]; then inventory_count=$(ls -1 /var/lib/secubox-factory/inventory/*.json 2>/dev/null | wc -l) fi json_add_int "inventory_count" "$inventory_count" json_dump } do_toggle_discovery() { local input enabled read input enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null) json_init if [ "$enabled" = "true" ] || [ "$enabled" = "1" ]; then uci -q set master-link.main.discovery_mode='1' else uci -q set master-link.main.discovery_mode='0' fi uci commit master-link local new_state=$(uci -q get master-link.main.discovery_mode) json_add_boolean "success" 1 json_add_boolean "discovery_enabled" "$([ "$new_state" = "1" ] && echo 1 || echo 0)" json_dump } case "$1" in list) echo '{' echo '"status":{},' echo '"list_images":{},' echo '"list_tokens":{},' echo '"list_clones":{},' echo '"list_devices":{},' echo '"list_versions":{},' echo '"list_build_profiles":{},' 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","version":"String","profile":"String"},' echo '"tftp_start":{},' echo '"tftp_stop":{},' echo '"delete_token":{"token":"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 '"pending_devices":{},' echo '"approve_device":{"device_id":"String","profile":"String"},' echo '"reject_device":{"device_id":"String","reason":"String"},' echo '"bulk_tokens":{"count":"Number","profile":"String","ttl":"Number"},' echo '"inventory":{},' echo '"list_profiles":{},' echo '"import_preregistered":{"devices":"Object"},' echo '"discovery_status":{},' echo '"toggle_discovery":{"enabled":"Boolean"},' echo '"remote_prepare_flash":{"ip":"String","image":"String","keep_settings":"Boolean","token":"String"},' echo '"remote_confirm_flash":{"ip":"String"},' echo '"remote_flash_status":{"ip":"String"},' echo '"remote_cancel_flash":{"ip":"String"}' echo '}' ;; call) case "$2" in status) do_status ;; list_images) do_list_images ;; list_tokens) do_list_tokens ;; list_clones) do_list_clones ;; list_devices) do_list_devices ;; list_versions) do_list_versions ;; list_build_profiles) do_list_build_profiles ;; 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 ;; pending_devices) do_pending_devices ;; approve_device) do_approve_device ;; reject_device) do_reject_device ;; bulk_tokens) do_bulk_tokens ;; inventory) do_inventory ;; list_profiles) do_list_profiles ;; import_preregistered) do_import_preregistered ;; discovery_status) do_discovery_status ;; toggle_discovery) do_toggle_discovery ;; remote_prepare_flash) do_remote_prepare_flash ;; remote_confirm_flash) do_remote_confirm_flash ;; remote_flash_status) do_remote_flash_status ;; remote_cancel_flash) do_remote_cancel_flash ;; esac ;; esac