feat(tools): Add SecuBox image builder and sysupgrade scripts
secubox-image.sh: Dev-side image builder via OpenWrt ASU API with build, download, firmware-selector commands. Includes first-boot script that resizes root, adds SecuBox feed, and installs all packages. Supports --resize flag for full eMMC utilization. secubox-sysupgrade.sh: On-device upgrade script that detects current device/packages, builds custom image via ASU, and applies sysupgrade. Uses jsonfilter (OpenWrt native) for JSON parsing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3c1d6d2fd0
commit
cd4e991761
673
secubox-tools/secubox-image.sh
Executable file
673
secubox-tools/secubox-image.sh
Executable file
@ -0,0 +1,673 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# secubox-image.sh - Build SecuBox firmware images via OpenWrt ASU API
|
||||
#
|
||||
# Uses the Attended SysUpgrade server (firmware-selector.openwrt.org backend)
|
||||
# to build custom OpenWrt images with all required packages, maximum rootfs,
|
||||
# and a first-boot script that installs SecuBox packages from the feed.
|
||||
#
|
||||
# Usage:
|
||||
# ./secubox-image.sh build [device] # Build via ASU API
|
||||
# ./secubox-image.sh firmware-selector [dev] # Print config for web UI paste
|
||||
# ./secubox-image.sh status <hash> # Check build status
|
||||
# ./secubox-image.sh download <hash> # Download completed build
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ASU_URL="https://sysupgrade.openwrt.org"
|
||||
OPENWRT_VERSION="24.10.5"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/build/images"
|
||||
DEFAULT_DEVICE="mochabin"
|
||||
FEED_URL="https://github.com/gkerma/secubox-openwrt/releases/latest/download"
|
||||
RESIZE_TARGET="" # e.g. "16G" — resize image after download
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
log_step() { echo -e "${CYAN}[STEP]${NC} $*" >&2; }
|
||||
|
||||
# Device profiles: device_name -> target:profile
|
||||
declare -A DEVICES=(
|
||||
["mochabin"]="mvebu/cortexa72:globalscale_mochabin"
|
||||
["espressobin-v7"]="mvebu/cortexa53:globalscale_espressobin"
|
||||
["espressobin-ultra"]="mvebu/cortexa53:globalscale_espressobin-ultra"
|
||||
["x86-64"]="x86/64:generic"
|
||||
)
|
||||
|
||||
# Device-specific extra packages
|
||||
declare -A DEVICE_PACKAGES=(
|
||||
["mochabin"]="kmod-sfp kmod-phy-marvell-10g"
|
||||
["espressobin-v7"]=""
|
||||
["espressobin-ultra"]="kmod-mt76 kmod-mac80211"
|
||||
["x86-64"]=""
|
||||
)
|
||||
|
||||
# Base packages included in the firmware image (official OpenWrt repos)
|
||||
BASE_PACKAGES=(
|
||||
# LuCI
|
||||
luci luci-ssl luci-theme-bootstrap luci-app-firewall
|
||||
|
||||
# DNS
|
||||
dnsmasq-full -dnsmasq
|
||||
|
||||
# Networking essentials
|
||||
curl wget-ssl ca-certificates openssl-util
|
||||
rsync diffutils
|
||||
|
||||
# WireGuard VPN
|
||||
wireguard-tools luci-proto-wireguard kmod-wireguard
|
||||
|
||||
# Reverse proxy & cache
|
||||
haproxy squid
|
||||
|
||||
# MQTT
|
||||
mosquitto-client-ssl
|
||||
|
||||
# Container runtimes
|
||||
docker dockerd containerd
|
||||
lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy
|
||||
|
||||
# Storage / filesystem tools
|
||||
block-mount kmod-fs-ext4 e2fsprogs parted losetup
|
||||
|
||||
# Shell utilities
|
||||
nano htop lsblk
|
||||
|
||||
# Attended sysupgrade client (for future upgrades)
|
||||
owut attendedsysupgrade-common
|
||||
)
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: secubox-image.sh <command> [options]
|
||||
|
||||
Commands:
|
||||
build [device] Build firmware image via ASU API (default: mochabin)
|
||||
firmware-selector [dev] Print packages + script for firmware-selector.openwrt.org
|
||||
status <hash> Check build status
|
||||
download <hash> Download completed build
|
||||
|
||||
Devices:
|
||||
mochabin MOCHAbin (Cortex-A72, 10G) — default
|
||||
espressobin-v7 ESPRESSObin V7 (Cortex-A53)
|
||||
espressobin-ultra ESPRESSObin Ultra (Cortex-A53, WiFi)
|
||||
x86-64 Generic x86_64
|
||||
|
||||
Options:
|
||||
-v, --version VERSION OpenWrt version (default: 24.10.5)
|
||||
-o, --output DIR Output directory (default: secubox-tools/build/images/)
|
||||
-r, --resize SIZE Resize image after download (e.g., 16G for eMMC)
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse device profile into target and profile
|
||||
parse_device() {
|
||||
local device="${1:-$DEFAULT_DEVICE}"
|
||||
|
||||
if [[ -z "${DEVICES[$device]+x}" ]]; then
|
||||
log_error "Unknown device: $device"
|
||||
log_info "Available: ${!DEVICES[*]}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local spec="${DEVICES[$device]}"
|
||||
TARGET="${spec%%:*}"
|
||||
PROFILE="${spec##*:}"
|
||||
DEVICE="$device"
|
||||
|
||||
log_info "Device: $DEVICE ($TARGET / $PROFILE)"
|
||||
}
|
||||
|
||||
# Build the full package list for a device
|
||||
get_packages() {
|
||||
local device="${1:-$DEFAULT_DEVICE}"
|
||||
local pkgs=("${BASE_PACKAGES[@]}")
|
||||
|
||||
# Add device-specific packages
|
||||
local extras="${DEVICE_PACKAGES[$device]:-}"
|
||||
if [[ -n "$extras" ]]; then
|
||||
for pkg in $extras; do
|
||||
pkgs+=("$pkg")
|
||||
done
|
||||
fi
|
||||
|
||||
echo "${pkgs[*]}"
|
||||
}
|
||||
|
||||
# Generate the first-boot defaults script
|
||||
generate_defaults() {
|
||||
cat <<'DEFAULTS'
|
||||
#!/bin/sh
|
||||
# SecuBox First Boot Setup — runs once after flash/sysupgrade
|
||||
|
||||
FEED_URL="https://github.com/gkerma/secubox-openwrt/releases/latest/download"
|
||||
LOG="/tmp/secubox-setup.log"
|
||||
|
||||
log() { echo "[$(date +%T)] $*" | tee -a "$LOG"; logger -t secubox-setup "$*"; }
|
||||
log "SecuBox first-boot setup starting..."
|
||||
|
||||
# --- Step 1: Resize root to fill storage ---
|
||||
log "Resizing root partition..."
|
||||
ROOT_DEV=$(awk '$2=="/" {print $1}' /proc/mounts)
|
||||
if [ -n "$ROOT_DEV" ]; then
|
||||
DISK=$(echo "$ROOT_DEV" | sed 's/p\?[0-9]*$//')
|
||||
[ "$DISK" = "$ROOT_DEV" ] && DISK=$(echo "$ROOT_DEV" | sed 's/[0-9]*$//')
|
||||
PART_NUM=$(echo "$ROOT_DEV" | grep -o '[0-9]*$')
|
||||
if command -v parted >/dev/null 2>&1; then
|
||||
parted -s "$DISK" resizepart "$PART_NUM" 100% 2>/dev/null
|
||||
resize2fs "$ROOT_DEV" 2>/dev/null
|
||||
log "Root resized: $(df -h / | tail -1 | awk '{print $2}')"
|
||||
else
|
||||
log "parted not available, skipping resize"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Step 2: Add SecuBox package feed ---
|
||||
log "Adding SecuBox feed..."
|
||||
FEED_CONF="/etc/opkg/customfeeds.conf"
|
||||
grep -q "secubox" "$FEED_CONF" 2>/dev/null || \
|
||||
echo "src/gz secubox $FEED_URL" >> "$FEED_CONF"
|
||||
opkg update >>"$LOG" 2>&1
|
||||
|
||||
# --- Step 3: Install SecuBox packages ---
|
||||
log "Installing SecuBox core packages..."
|
||||
CORE_PKGS="secubox-core secubox-app secubox-p2p luci-app-secubox luci-theme-secubox"
|
||||
CORE_PKGS="$CORE_PKGS luci-app-secubox-admin luci-app-secubox-portal luci-app-system-hub"
|
||||
CORE_PKGS="$CORE_PKGS luci-app-service-registry secubox-master-link luci-app-master-link"
|
||||
for pkg in $CORE_PKGS; do
|
||||
opkg install "$pkg" >>"$LOG" 2>&1 || log "WARN: $pkg failed"
|
||||
done
|
||||
|
||||
log "Installing all SecuBox packages from feed..."
|
||||
opkg list 2>/dev/null | awk '/^(secubox-|luci-app-secubox|luci-app-master|luci-app-service|luci-app-auth|luci-app-bandwidth|luci-app-cdn|luci-app-client|luci-app-crowdsec|luci-app-cyber|luci-app-dns|luci-app-exposure|luci-app-gitea|luci-app-glances|luci-app-haproxy|luci-app-hexo|luci-app-jitsi|luci-app-ksm|luci-app-local|luci-app-lyrion|luci-app-magic|luci-app-mail|luci-app-media|luci-app-meta|luci-app-mitmproxy|luci-app-mmpm|luci-app-mqtt|luci-app-ndpid|luci-app-netd|luci-app-network|luci-app-next|luci-app-ollama|luci-app-pico|luci-app-simplex|luci-app-stream|luci-app-system-hub|luci-app-tor|luci-app-traffic|luci-app-vhost|luci-app-wireguard-dash|luci-app-zigbee|luci-secubox)/{print $1}' | \
|
||||
while read -r pkg; do
|
||||
opkg install "$pkg" >>"$LOG" 2>&1 || true
|
||||
done
|
||||
|
||||
# --- Step 4: Enable core services ---
|
||||
for svc in secubox-core; do
|
||||
[ -x "/etc/init.d/$svc" ] && /etc/init.d/$svc enable
|
||||
done
|
||||
|
||||
# --- Step 5: Ensure sysupgrade config preserves SecuBox data ---
|
||||
SYSUPGRADE_CONF="/etc/sysupgrade.conf"
|
||||
for path in /etc/config/ /etc/secubox/ /etc/opkg/customfeeds.conf /srv/; do
|
||||
grep -q "^${path}$" "$SYSUPGRADE_CONF" 2>/dev/null || echo "$path" >> "$SYSUPGRADE_CONF"
|
||||
done
|
||||
|
||||
INSTALLED=$(opkg list-installed 2>/dev/null | grep -c secubox || echo 0)
|
||||
log "SecuBox setup complete — $INSTALLED packages installed"
|
||||
log "Disk usage: $(df -h / | tail -1 | awk '{print $3 "/" $2 " (" $5 ")"}')"
|
||||
exit 0
|
||||
DEFAULTS
|
||||
}
|
||||
|
||||
# Build JSON request for ASU API
|
||||
build_request_json() {
|
||||
local device="${1:-$DEFAULT_DEVICE}"
|
||||
parse_device "$device"
|
||||
|
||||
local packages
|
||||
packages=$(get_packages "$device")
|
||||
|
||||
# Write defaults to temp file for python to read
|
||||
local defaults_file
|
||||
defaults_file=$(mktemp)
|
||||
generate_defaults > "$defaults_file"
|
||||
|
||||
# Build JSON package array
|
||||
local pkg_json=""
|
||||
for pkg in $packages; do
|
||||
[[ -n "$pkg_json" ]] && pkg_json="$pkg_json,"
|
||||
pkg_json="$pkg_json\"$pkg\""
|
||||
done
|
||||
|
||||
# Encode defaults as JSON string
|
||||
local defaults_encoded
|
||||
defaults_encoded=$(python3 -c "
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
print(json.dumps(f.read()))
|
||||
" "$defaults_file")
|
||||
rm -f "$defaults_file"
|
||||
|
||||
# Build complete JSON via python for correctness
|
||||
python3 -c "
|
||||
import json, sys
|
||||
data = {
|
||||
'profile': '$PROFILE',
|
||||
'target': '$TARGET',
|
||||
'version': '$OPENWRT_VERSION',
|
||||
'packages': '$packages'.split(),
|
||||
'defaults': json.loads(sys.argv[1]),
|
||||
'rootfs_size_mb': 1024,
|
||||
'diff_packages': True,
|
||||
'client': 'secubox-image/1.0'
|
||||
}
|
||||
print(json.dumps(data, indent=2))
|
||||
" "$defaults_encoded"
|
||||
}
|
||||
|
||||
# POST build request to ASU API
|
||||
api_build() {
|
||||
local json_file="$1"
|
||||
log_step "Submitting build request to ASU..."
|
||||
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "@$json_file" \
|
||||
"$ASU_URL/api/v1/build")
|
||||
|
||||
local http_code
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
local body
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
case "$http_code" in
|
||||
200)
|
||||
log_info "Build completed (cached)"
|
||||
echo "$body"
|
||||
;;
|
||||
202)
|
||||
log_info "Build queued"
|
||||
echo "$body"
|
||||
;;
|
||||
400)
|
||||
log_error "Bad request:"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
422)
|
||||
log_error "Validation error:"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
500)
|
||||
log_error "Server error:"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
log_error "Unexpected HTTP $http_code:"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Poll build status until complete
|
||||
poll_build() {
|
||||
local request_hash="$1"
|
||||
local max_wait=600 # 10 minutes
|
||||
local interval=10
|
||||
local elapsed=0
|
||||
|
||||
log_step "Waiting for build to complete (hash: $request_hash)..."
|
||||
|
||||
while [ $elapsed -lt $max_wait ]; do
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" "$ASU_URL/api/v1/build/$request_hash")
|
||||
local http_code
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
local body
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
# Check if build has images (= complete)
|
||||
local detail
|
||||
detail=$(echo "$body" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get("detail",""))' 2>/dev/null || echo "")
|
||||
|
||||
if [ "$detail" = "done" ] || [ "$detail" = "" ]; then
|
||||
# Check for images to confirm completion
|
||||
local img_count
|
||||
img_count=$(echo "$body" | python3 -c 'import sys,json; print(len(json.load(sys.stdin).get("images",[])))' 2>/dev/null || echo "0")
|
||||
if [ "$img_count" -gt 0 ] 2>/dev/null; then
|
||||
log_info "Build complete!"
|
||||
echo "$body"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$detail" in
|
||||
"queued"|"building")
|
||||
printf "\r Waiting... %ds elapsed (%s) " "$elapsed" "$detail" >&2
|
||||
;;
|
||||
*)
|
||||
# Response has no images but detail is not queued/building
|
||||
log_info "Build finished (detail: $detail)"
|
||||
echo "$body"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
elif [ "$http_code" = "202" ]; then
|
||||
printf "\r Waiting... %ds elapsed (building) " "$elapsed" >&2
|
||||
elif [ "$http_code" = "404" ]; then
|
||||
log_error "Build not found: $request_hash"
|
||||
return 1
|
||||
else
|
||||
log_error "Unexpected HTTP $http_code"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep "$interval"
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
|
||||
echo ""
|
||||
log_error "Build timed out after ${max_wait}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Download the built image
|
||||
download_image() {
|
||||
local build_response="$1"
|
||||
local output_dir="${2:-$OUTPUT_DIR}"
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Save response to temp file for python to parse
|
||||
local resp_file
|
||||
resp_file=$(mktemp)
|
||||
echo "$build_response" > "$resp_file"
|
||||
|
||||
# Extract image name: prefer ext4-sdcard, then sysupgrade, then any sdcard
|
||||
local image_name
|
||||
image_name=$(python3 -c "
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
d = json.load(f)
|
||||
images = d.get('images', [])
|
||||
# Prefer ext4-sdcard (best for resize)
|
||||
for img in images:
|
||||
if 'ext4' in img.get('name', '') and 'sdcard' in img.get('name', ''):
|
||||
print(img['name']); sys.exit()
|
||||
# Then sysupgrade
|
||||
for img in images:
|
||||
if 'sysupgrade' in img.get('name', '') or 'sysupgrade' in img.get('type', ''):
|
||||
print(img['name']); sys.exit()
|
||||
# Then any sdcard
|
||||
for img in images:
|
||||
if 'sdcard' in img.get('name', '') or 'sdcard' in img.get('type', ''):
|
||||
print(img['name']); sys.exit()
|
||||
# Fallback: first image
|
||||
if images:
|
||||
print(images[0]['name'])
|
||||
" "$resp_file" 2>/dev/null)
|
||||
|
||||
# Extract request_hash (used as store directory) and expected sha256
|
||||
local request_hash expected_sha256
|
||||
read -r request_hash expected_sha256 < <(python3 -c "
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
d = json.load(f)
|
||||
rh = d.get('request_hash', '')
|
||||
# Find sha256 for the selected image
|
||||
sha = ''
|
||||
for img in d.get('images', []):
|
||||
if img.get('name', '') == sys.argv[2]:
|
||||
sha = img.get('sha256', '')
|
||||
break
|
||||
print(rh, sha)
|
||||
" "$resp_file" "$image_name" 2>/dev/null)
|
||||
|
||||
rm -f "$resp_file"
|
||||
|
||||
if [[ -z "$image_name" ]]; then
|
||||
log_warn "No images found in build response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$request_hash" ]]; then
|
||||
log_error "No request_hash in build response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ASU store URL uses request_hash as directory
|
||||
local download_url="$ASU_URL/store/$request_hash/$image_name"
|
||||
local filename="$image_name"
|
||||
local output_file="$output_dir/$filename"
|
||||
|
||||
log_step "Downloading: $filename"
|
||||
log_info "URL: $download_url"
|
||||
curl -# -o "$output_file" "$download_url" || {
|
||||
log_error "Download failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Verify size is non-zero
|
||||
local file_size
|
||||
file_size=$(stat -c%s "$output_file" 2>/dev/null || echo 0)
|
||||
if [[ "$file_size" -eq 0 ]]; then
|
||||
log_error "Downloaded file is empty"
|
||||
rm -f "$output_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify SHA256 if available
|
||||
local sha256
|
||||
sha256=$(sha256sum "$output_file" | awk '{print $1}')
|
||||
log_info "Downloaded: $output_file"
|
||||
log_info "SHA256: $sha256"
|
||||
log_info "Size: $(du -h "$output_file" | awk '{print $1}')"
|
||||
|
||||
if [[ -n "$expected_sha256" && "$sha256" != "$expected_sha256" ]]; then
|
||||
log_warn "SHA256 mismatch! Expected: $expected_sha256"
|
||||
elif [[ -n "$expected_sha256" ]]; then
|
||||
log_info "SHA256 verified OK"
|
||||
fi
|
||||
|
||||
echo "$output_file"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Commands
|
||||
# =============================================================================
|
||||
|
||||
cmd_build() {
|
||||
local device="${1:-$DEFAULT_DEVICE}"
|
||||
|
||||
# Check dependencies
|
||||
for cmd in curl python3; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || {
|
||||
log_error "Required: $cmd"
|
||||
return 1
|
||||
}
|
||||
done
|
||||
|
||||
echo -e "${BOLD}SecuBox Image Builder${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Generate request
|
||||
log_step "Generating build request for $device..."
|
||||
local json
|
||||
json=$(build_request_json "$device")
|
||||
|
||||
local json_file
|
||||
json_file=$(mktemp)
|
||||
echo "$json" > "$json_file"
|
||||
trap "rm -f $json_file" EXIT
|
||||
|
||||
local pkg_count
|
||||
pkg_count=$(echo "$json" | python3 -c 'import sys,json; print(len(json.load(sys.stdin)["packages"]))' 2>/dev/null || echo "?")
|
||||
log_info "OpenWrt: $OPENWRT_VERSION"
|
||||
log_info "Rootfs: 1024 MB (maximum)"
|
||||
log_info "Packages: $pkg_count official + SecuBox feed at boot"
|
||||
|
||||
# Submit build
|
||||
local response
|
||||
response=$(api_build "$json_file") || return 1
|
||||
|
||||
# Check if response already has images (cached build) or needs polling
|
||||
local has_images request_hash
|
||||
has_images=$(echo "$response" | python3 -c 'import sys,json; print(len(json.load(sys.stdin).get("images",[])))' 2>/dev/null || echo "0")
|
||||
request_hash=$(echo "$response" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("request_hash",""))' 2>/dev/null || echo "")
|
||||
|
||||
local image_file=""
|
||||
local build_result="$response"
|
||||
|
||||
if [[ "$has_images" -gt 0 ]]; then
|
||||
# Cached build — already has images, skip polling
|
||||
image_file=$(download_image "$build_result") || return 1
|
||||
elif [[ -n "$request_hash" ]]; then
|
||||
# Need to poll for completion
|
||||
build_result=$(poll_build "$request_hash") || return 1
|
||||
echo "" >&2
|
||||
image_file=$(download_image "$build_result") || return 1
|
||||
else
|
||||
log_error "No images or request hash in response"
|
||||
echo "$response" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Resize image if requested (for full eMMC utilization)
|
||||
if [[ -n "$RESIZE_TARGET" && -n "$image_file" ]]; then
|
||||
local resize_script="$SCRIPT_DIR/resize-openwrt-image.sh"
|
||||
if [[ ! -x "$resize_script" ]]; then
|
||||
log_error "Resize script not found: $resize_script"
|
||||
log_info "Image saved without resize: $image_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local resized_file="${image_file%.img.gz}-${RESIZE_TARGET}.img.gz"
|
||||
log_step "Resizing image to $RESIZE_TARGET for full eMMC..."
|
||||
|
||||
# resize-openwrt-image.sh requires root for loop devices
|
||||
local sudo_cmd=""
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
sudo_cmd="sudo"
|
||||
log_info "Root required for resize (loop devices). Running with sudo..."
|
||||
fi
|
||||
|
||||
$sudo_cmd "$resize_script" "$image_file" "$RESIZE_TARGET" "$resized_file" || {
|
||||
log_error "Resize failed"
|
||||
log_info "To resize manually: sudo $resize_script $image_file $RESIZE_TARGET $resized_file"
|
||||
log_info "Original image: $image_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_info "Resized image: $resized_file"
|
||||
log_info "Original image: $image_file"
|
||||
echo "$resized_file"
|
||||
else
|
||||
echo "$image_file"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_firmware_selector() {
|
||||
local device="${1:-$DEFAULT_DEVICE}"
|
||||
parse_device "$device"
|
||||
|
||||
local packages
|
||||
packages=$(get_packages "$device")
|
||||
|
||||
echo -e "${BOLD}SecuBox Firmware Selector Configuration${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo -e "${CYAN}1. Open:${NC} https://firmware-selector.openwrt.org/"
|
||||
echo -e "${CYAN}2. Select version:${NC} $OPENWRT_VERSION"
|
||||
echo -e "${CYAN}3. Search device:${NC} $DEVICE"
|
||||
echo -e "${CYAN}4. Click:${NC} 'Customize installed packages and/or first boot script'"
|
||||
echo ""
|
||||
echo -e "${BOLD}═══ PACKAGES (paste into 'Installed Packages' field) ═══${NC}"
|
||||
echo ""
|
||||
echo "$packages"
|
||||
echo ""
|
||||
echo -e "${BOLD}═══ SCRIPT (paste into 'Script to run on first boot' field) ═══${NC}"
|
||||
echo ""
|
||||
generate_defaults
|
||||
echo ""
|
||||
echo -e "${BOLD}═══ ROOTFS SIZE ═══${NC}"
|
||||
echo ""
|
||||
echo "Set 'Root filesystem partition size' to: 1024 MB"
|
||||
echo ""
|
||||
echo -e "${CYAN}5. Click:${NC} 'Request Build'"
|
||||
echo -e "${CYAN}6. Download:${NC} SYSUPGRADE or SDCARD image"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local hash="$1"
|
||||
[[ -z "$hash" ]] && { log_error "Usage: $0 status <hash>"; return 1; }
|
||||
|
||||
local response
|
||||
response=$(curl -s "$ASU_URL/api/v1/build/$hash")
|
||||
echo "$response" | python3 -m json.tool 2>/dev/null || echo "$response"
|
||||
}
|
||||
|
||||
cmd_download() {
|
||||
local hash="$1"
|
||||
[[ -z "$hash" ]] && { log_error "Usage: $0 download <hash>"; return 1; }
|
||||
|
||||
local response
|
||||
response=$(curl -s "$ASU_URL/api/v1/build/$hash")
|
||||
download_image "$response"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
# Parse global options
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-v|--version)
|
||||
OPENWRT_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--resize)
|
||||
RESIZE_TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "${1:-}" in
|
||||
build)
|
||||
shift
|
||||
cmd_build "${1:-$DEFAULT_DEVICE}"
|
||||
;;
|
||||
firmware-selector|fs)
|
||||
shift
|
||||
cmd_firmware_selector "${1:-$DEFAULT_DEVICE}"
|
||||
;;
|
||||
status)
|
||||
shift
|
||||
cmd_status "${1:-}"
|
||||
;;
|
||||
download)
|
||||
shift
|
||||
cmd_download "${1:-}"
|
||||
;;
|
||||
help|--help|-h|"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
457
secubox-tools/secubox-sysupgrade.sh
Executable file
457
secubox-tools/secubox-sysupgrade.sh
Executable file
@ -0,0 +1,457 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# secubox-sysupgrade.sh - Upgrade a running SecuBox via OpenWrt ASU API
|
||||
#
|
||||
# Detects the current device, packages, and version, then requests a
|
||||
# custom sysupgrade image from the ASU server with all packages preserved.
|
||||
#
|
||||
# Usage:
|
||||
# secubox-sysupgrade check # Show current version + available upgrades
|
||||
# secubox-sysupgrade build # Request new image from ASU
|
||||
# secubox-sysupgrade upgrade # Build + download + sysupgrade
|
||||
# secubox-sysupgrade status # Show current device info
|
||||
#
|
||||
|
||||
ASU_URL="https://sysupgrade.openwrt.org"
|
||||
DOWNLOAD_DIR="/tmp"
|
||||
IMAGE_FILE="$DOWNLOAD_DIR/secubox-sysupgrade.img.gz"
|
||||
LOG="/tmp/secubox-sysupgrade.log"
|
||||
FEED_URL="https://github.com/gkerma/secubox-openwrt/releases/latest/download"
|
||||
|
||||
log() { echo "[$(date +%T)] $*" | tee -a "$LOG"; }
|
||||
log_info() { echo "[INFO] $*"; }
|
||||
log_warn() { echo "[WARN] $*" >&2; }
|
||||
log_error() { echo "[ERROR] $*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: secubox-sysupgrade <command>
|
||||
|
||||
Commands:
|
||||
check Show current version and check for updates
|
||||
build Request a new image from ASU (does not flash)
|
||||
upgrade Build + download + sysupgrade (flashes the device)
|
||||
status Show current device info
|
||||
|
||||
Options:
|
||||
-v VERSION Target OpenWrt version (default: current)
|
||||
-y Skip confirmation prompt for upgrade
|
||||
-h Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Device Detection
|
||||
# =============================================================================
|
||||
|
||||
detect_device() {
|
||||
# OpenWrt version
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
CURRENT_VERSION="${VERSION_ID:-unknown}"
|
||||
else
|
||||
CURRENT_VERSION="unknown"
|
||||
fi
|
||||
|
||||
# Target from board.json
|
||||
if [ -f /etc/board.json ] && command -v jsonfilter >/dev/null 2>&1; then
|
||||
BOARD_TARGET=$(jsonfilter -i /etc/board.json -e '@.target.name' 2>/dev/null || echo "")
|
||||
BOARD_PROFILE=$(jsonfilter -i /etc/board.json -e '@.model.id' 2>/dev/null || echo "")
|
||||
BOARD_NAME=$(jsonfilter -i /etc/board.json -e '@.model.name' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Fallback: read from /tmp/sysinfo
|
||||
[ -z "$BOARD_TARGET" ] && BOARD_TARGET=$(cat /tmp/sysinfo/board_name 2>/dev/null | sed 's/,.*//')
|
||||
|
||||
# Normalize profile: comma -> underscore
|
||||
BOARD_PROFILE=$(echo "$BOARD_PROFILE" | tr ',' '_')
|
||||
|
||||
# Installed packages (names only)
|
||||
INSTALLED_PACKAGES=$(opkg list-installed 2>/dev/null | awk '{print $1}' | sort)
|
||||
PACKAGE_COUNT=$(echo "$INSTALLED_PACKAGES" | wc -l)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Sysupgrade Config
|
||||
# =============================================================================
|
||||
|
||||
ensure_sysupgrade_conf() {
|
||||
local conf="/etc/sysupgrade.conf"
|
||||
for path in /etc/config/ /etc/secubox/ /etc/opkg/customfeeds.conf /srv/; do
|
||||
grep -q "^${path}$" "$conf" 2>/dev/null || echo "$path" >> "$conf"
|
||||
done
|
||||
}
|
||||
|
||||
# Ensure SecuBox feed is configured for post-upgrade
|
||||
ensure_feed() {
|
||||
local feed_conf="/etc/opkg/customfeeds.conf"
|
||||
grep -q "secubox" "$feed_conf" 2>/dev/null || {
|
||||
echo "src/gz secubox $FEED_URL" >> "$feed_conf"
|
||||
log "Added SecuBox feed to $feed_conf"
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# ASU API
|
||||
# =============================================================================
|
||||
|
||||
# Build JSON request body
|
||||
build_request() {
|
||||
local version="${1:-$CURRENT_VERSION}"
|
||||
local packages
|
||||
|
||||
# Convert package list to JSON array
|
||||
packages=$(echo "$INSTALLED_PACKAGES" | awk '{printf "\"%s\",", $1}' | sed 's/,$//')
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"profile": "$BOARD_PROFILE",
|
||||
"target": "$BOARD_TARGET",
|
||||
"version": "$version",
|
||||
"packages": [$packages],
|
||||
"rootfs_size_mb": 1024,
|
||||
"diff_packages": true,
|
||||
"client": "secubox-sysupgrade/1.0"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Submit build to ASU
|
||||
asu_build() {
|
||||
local version="${1:-$CURRENT_VERSION}"
|
||||
local json
|
||||
json=$(build_request "$version")
|
||||
|
||||
log "Submitting build request to ASU..."
|
||||
log " Target: $BOARD_TARGET"
|
||||
log " Profile: $BOARD_PROFILE"
|
||||
log " Version: $version"
|
||||
log " Packages: $PACKAGE_COUNT"
|
||||
|
||||
local tmpfile="/tmp/secubox-asu-request.json"
|
||||
echo "$json" > "$tmpfile"
|
||||
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "@$tmpfile" \
|
||||
"$ASU_URL/api/v1/build" 2>>"$LOG")
|
||||
|
||||
local http_code
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
local body
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
rm -f "$tmpfile"
|
||||
|
||||
case "$http_code" in
|
||||
200)
|
||||
log_info "Build completed (cached)"
|
||||
echo "$body"
|
||||
return 0
|
||||
;;
|
||||
202)
|
||||
log_info "Build queued, waiting..."
|
||||
# Extract request hash and poll
|
||||
local hash
|
||||
hash=$(echo "$body" | jsonfilter -e '@.request_hash' 2>/dev/null || echo "")
|
||||
if [ -n "$hash" ]; then
|
||||
poll_build "$hash"
|
||||
return $?
|
||||
fi
|
||||
echo "$body"
|
||||
return 0
|
||||
;;
|
||||
400|422)
|
||||
log_error "Build request rejected:"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
500)
|
||||
log_error "ASU server error"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
log_error "Unexpected HTTP $http_code"
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Poll build status
|
||||
poll_build() {
|
||||
local hash="$1"
|
||||
local max_wait=600
|
||||
local interval=10
|
||||
local elapsed=0
|
||||
|
||||
while [ $elapsed -lt $max_wait ]; do
|
||||
local response
|
||||
response=$(curl -s "$ASU_URL/api/v1/build/$hash" 2>/dev/null)
|
||||
|
||||
# Check if response has images (= complete)
|
||||
local image_count
|
||||
image_count=$(echo "$response" | jsonfilter -e '@.images[*]' 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$image_count" -gt 0 ] 2>/dev/null; then
|
||||
log_info "Build complete!"
|
||||
echo "$response"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "\r Building... %ds elapsed " "$elapsed"
|
||||
sleep "$interval"
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
|
||||
echo ""
|
||||
log_error "Build timed out after ${max_wait}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Download sysupgrade image from build response
|
||||
download_image() {
|
||||
local build_response="$1"
|
||||
|
||||
# Find sysupgrade image name
|
||||
local image_name=""
|
||||
local image_sha256=""
|
||||
|
||||
# Find best image: prefer sysupgrade, then ext4-sdcard, then any sdcard
|
||||
local images
|
||||
images=$(echo "$build_response" | jsonfilter -e '@.images[*].name' 2>/dev/null)
|
||||
|
||||
for name in $images; do
|
||||
case "$name" in
|
||||
*sysupgrade*)
|
||||
image_name="$name"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Fallback: prefer ext4-sdcard (better for resize)
|
||||
if [ -z "$image_name" ]; then
|
||||
for name in $images; do
|
||||
case "$name" in
|
||||
*ext4*sdcard*)
|
||||
image_name="$name"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
# Fallback: any sdcard
|
||||
if [ -z "$image_name" ]; then
|
||||
for name in $images; do
|
||||
case "$name" in
|
||||
*sdcard*)
|
||||
image_name="$name"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$image_name" ]; then
|
||||
log_error "No sysupgrade or sdcard image found in build"
|
||||
log_error "Available images:"
|
||||
echo "$images" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract request_hash (used as store directory in ASU)
|
||||
local request_hash
|
||||
request_hash=$(echo "$build_response" | jsonfilter -e '@.request_hash' 2>/dev/null || echo "")
|
||||
|
||||
local download_url
|
||||
if [ -n "$request_hash" ]; then
|
||||
download_url="$ASU_URL/store/$request_hash/$image_name"
|
||||
else
|
||||
# Fallback: try image_prefix
|
||||
local image_prefix
|
||||
image_prefix=$(echo "$build_response" | jsonfilter -e '@.image_prefix' 2>/dev/null || echo "")
|
||||
download_url="$ASU_URL/store/$image_prefix/$image_name"
|
||||
fi
|
||||
|
||||
log "Downloading: $image_name"
|
||||
curl -# -o "$IMAGE_FILE" "$download_url" 2>>"$LOG" || {
|
||||
log_error "Download failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
local size
|
||||
size=$(ls -lh "$IMAGE_FILE" 2>/dev/null | awk '{print $5}')
|
||||
log_info "Downloaded: $IMAGE_FILE ($size)"
|
||||
|
||||
# Verify checksum if available
|
||||
local expected_sha
|
||||
# Find sha256 for the sysupgrade image
|
||||
expected_sha=$(echo "$build_response" | jsonfilter -e '@.images[*]' 2>/dev/null | \
|
||||
while read -r line; do
|
||||
echo "$line"
|
||||
done | grep -A1 "$image_name" | grep sha256 | head -1)
|
||||
|
||||
if [ -n "$expected_sha" ]; then
|
||||
local actual_sha
|
||||
actual_sha=$(sha256sum "$IMAGE_FILE" | awk '{print $1}')
|
||||
log_info "SHA256: $actual_sha"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Commands
|
||||
# =============================================================================
|
||||
|
||||
cmd_status() {
|
||||
detect_device
|
||||
|
||||
echo "SecuBox Device Info"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " OpenWrt Version : $CURRENT_VERSION"
|
||||
echo " Target : $BOARD_TARGET"
|
||||
echo " Profile : $BOARD_PROFILE"
|
||||
echo " Board : $BOARD_NAME"
|
||||
echo " Packages : $PACKAGE_COUNT installed"
|
||||
echo " SecuBox pkgs : $(echo "$INSTALLED_PACKAGES" | grep -c secubox) installed"
|
||||
echo " Disk usage : $(df -h / | tail -1 | awk '{print $3 "/" $2 " (" $5 ")"}')"
|
||||
echo " Uptime : $(uptime | sed 's/.*up //' | sed 's/,.*load.*//')"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
detect_device
|
||||
|
||||
echo "Current: OpenWrt $CURRENT_VERSION ($BOARD_TARGET / $BOARD_PROFILE)"
|
||||
echo "Packages: $PACKAGE_COUNT installed"
|
||||
echo ""
|
||||
|
||||
# Check ASU for available versions
|
||||
log_info "Checking ASU server for available versions..."
|
||||
local overview
|
||||
overview=$(curl -s "$ASU_URL/api/v1/overview" 2>/dev/null)
|
||||
|
||||
if [ -n "$overview" ]; then
|
||||
# Try to extract version info
|
||||
local branches
|
||||
branches=$(echo "$overview" | jsonfilter -e '@.branches[*]' 2>/dev/null || echo "")
|
||||
if [ -n "$branches" ]; then
|
||||
echo "Available branches:"
|
||||
echo "$branches" | while read -r branch; do
|
||||
echo " - $branch"
|
||||
done
|
||||
else
|
||||
log_info "ASU server reachable"
|
||||
fi
|
||||
else
|
||||
log_warn "Could not reach ASU server"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_build() {
|
||||
detect_device
|
||||
|
||||
local version="${TARGET_VERSION:-$CURRENT_VERSION}"
|
||||
|
||||
log_info "Building image for $BOARD_PROFILE ($BOARD_TARGET) v$version"
|
||||
log_info "$PACKAGE_COUNT packages will be preserved"
|
||||
echo ""
|
||||
|
||||
local result
|
||||
result=$(asu_build "$version") || return 1
|
||||
|
||||
download_image "$result" || return 1
|
||||
|
||||
echo ""
|
||||
log_info "Image ready: $IMAGE_FILE"
|
||||
log_info "To flash: sysupgrade -v $IMAGE_FILE"
|
||||
}
|
||||
|
||||
cmd_upgrade() {
|
||||
detect_device
|
||||
ensure_sysupgrade_conf
|
||||
ensure_feed
|
||||
|
||||
local version="${TARGET_VERSION:-$CURRENT_VERSION}"
|
||||
local skip_confirm="${SKIP_CONFIRM:-0}"
|
||||
|
||||
echo "SecuBox Sysupgrade"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Current : OpenWrt $CURRENT_VERSION"
|
||||
echo " Target : OpenWrt $version"
|
||||
echo " Device : $BOARD_PROFILE ($BOARD_TARGET)"
|
||||
echo " Packages: $PACKAGE_COUNT"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
if [ "$skip_confirm" != "1" ]; then
|
||||
printf "Proceed with sysupgrade? [y/N] "
|
||||
read -r confirm
|
||||
case "$confirm" in
|
||||
y|Y|yes|YES) ;;
|
||||
*) log_info "Aborted"; return 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Build
|
||||
log_info "Step 1/3: Building image..."
|
||||
local result
|
||||
result=$(asu_build "$version") || return 1
|
||||
|
||||
# Download
|
||||
log_info "Step 2/3: Downloading image..."
|
||||
download_image "$result" || return 1
|
||||
|
||||
# Sysupgrade
|
||||
log_info "Step 3/3: Applying sysupgrade..."
|
||||
log_info "The device will reboot. Reconnect in ~60 seconds."
|
||||
echo ""
|
||||
|
||||
sysupgrade -v "$IMAGE_FILE"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
TARGET_VERSION=""
|
||||
SKIP_CONFIRM=0
|
||||
|
||||
# Parse options
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-v)
|
||||
TARGET_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-y)
|
||||
SKIP_CONFIRM=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "${1:-}" in
|
||||
check) cmd_check ;;
|
||||
build) cmd_build ;;
|
||||
upgrade) cmd_upgrade ;;
|
||||
status) cmd_status ;;
|
||||
help|--help|-h|"") usage ;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user