feat(cloner): Add SecuBox Station Cloner/Deployer
Host-side orchestrator (secubox-clone-station.sh): - Dual USB serial control with MOKATOOL integration - ASU API firmware building for clone images - TFTP serving with auto-generated U-Boot commands - Full workflow: detect → pull → flash → verify On-device CLI (secubox-cloner): - Build ext4 images for same device type - TFTP server management via dnsmasq - Clone token generation with auto-approve - Integration with master-link mesh onboarding First-boot provisioning (50-secubox-clone-provision): - Partition resize to full disk (parted + resize2fs) - Master discovery via mDNS/network scan - Automatic mesh join with pre-approved tokens Master-link enhancements: - ml_clone_token_generate() for 24h auto-approve tokens - ml_token_is_auto_approve() for token type detection - Auto-approve logic in join request handler SecuBox CLI additions: - secubox clone (build, serve, token, status, list, export) - secubox master-link (status, peers, token, join, approve) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
320277b87f
commit
8015d790e0
@ -944,3 +944,49 @@ _Last updated: 2026-02-07_
|
||||
- Fixed port conflict: console (8515), yijing360 (8521)
|
||||
- Deployed yijing-360.zip with generator.py
|
||||
- Emancipated: yijing360.gk2.secubox.in with SSL
|
||||
|
||||
26. **HAProxy Multi-Certificate SNI Fix (2026-02-07)**
|
||||
- Fixed multi-domain SSL certificate handling using `crt-list` instead of directory mode
|
||||
- Added `generate_certs_list()` function in haproxyctl to create certs.list from .pem files
|
||||
- Updated `haproxy-sync-certs` to regenerate certs.list after syncing ACME certs
|
||||
- HTTPS frontend now uses `crt-list /opt/haproxy/certs/certs.list` for reliable SNI matching
|
||||
- Each certificate's SANs and CN are extracted to create explicit domain-to-cert mappings
|
||||
- Fallback to directory mode if certs.list doesn't exist (backwards compatible)
|
||||
|
||||
27. **HAProxy Backend IP Fix (2026-02-07)**
|
||||
- Fixed localhost (127.0.0.1) usage in HAProxy backends - must use 192.168.255.1 (host bridge IP)
|
||||
- HAProxy runs in LXC container, cannot reach host services via 127.0.0.1
|
||||
- Added auto-conversion in RPCD handler: 127.0.0.1/localhost → 192.168.255.1
|
||||
- Fixed CLI tools: secubox-exposure, jellyfinctl, jitsctl, simplexctl, secubox-subdomain
|
||||
- Fixed Fabricator Streamlit Services page backend creation
|
||||
- Fixed HAProxy config templates for jitsi
|
||||
|
||||
28. **Station Cloner/Deployer Implementation (2026-02-08)**
|
||||
- Created `secubox-tools/secubox-clone-station.sh` — host-side cloning orchestrator for dual USB serial.
|
||||
- Commands: detect, pull, flash, verify, clone (full workflow), console, uboot, env-backup
|
||||
- Integrates with MOKATOOL (`mochabin_tool.py`) for serial console automation
|
||||
- Uses ASU API (firmware-selector.openwrt.org) for building clone images
|
||||
- TFTP serving for network boot with auto-generated U-Boot commands
|
||||
- Created `secubox-core/root/usr/sbin/secubox-cloner` — on-device clone manager CLI.
|
||||
- Commands: build, serve, token, status, list, export
|
||||
- Builds ext4 images for same device type (required for partition resize)
|
||||
- Generates clone provision scripts for TFTP download
|
||||
- Integrates with master-link for mesh join tokens
|
||||
- Created `secubox-core/root/etc/uci-defaults/50-secubox-clone-provision` — first-boot provisioning.
|
||||
- Step 1: Resize root partition to full disk (parted + resize2fs)
|
||||
- Step 2: Discover master via mDNS or network scan
|
||||
- Step 3: Configure as mesh peer (master-link UCI)
|
||||
- Step 4: Join mesh with token or request approval
|
||||
- Enhanced `secubox-master-link`:
|
||||
- Added `ml_clone_token_generate()` for auto-approve clone tokens (24h TTL)
|
||||
- Added `ml_token_is_auto_approve()` for token type detection
|
||||
- Updated `ml_join_request()` to auto-approve clone tokens
|
||||
- New CLI commands: clone-token, register-token
|
||||
- Updated `secubox` CLI:
|
||||
- Added `secubox clone` command group (build, serve, token, status, list, export)
|
||||
- Added `secubox master-link` command group (status, peers, token, clone-token, join, approve, pending)
|
||||
- **Clone workflow**:
|
||||
1. Master: `secubox clone build && secubox clone serve --start`
|
||||
2. Host: `./secubox-clone-station.sh clone` (detects, pulls, flashes target)
|
||||
3. Target boots, resizes root, auto-joins mesh with pre-approved token
|
||||
- Part of v0.19 mesh deployment automation.
|
||||
|
||||
@ -51,7 +51,15 @@ _Last updated: 2026-02-07_
|
||||
- Gossip-based exposure config sync via secubox-p2p
|
||||
- Created `luci-app-vortex-dns` dashboard
|
||||
|
||||
### Just Completed (2026-02-06/07)
|
||||
### Just Completed (2026-02-06/08)
|
||||
|
||||
- **Station Cloner/Deployer** — DONE (2026-02-08)
|
||||
- Host-side `secubox-clone-station.sh` with MOKATOOL integration for dual USB serial control
|
||||
- On-device `secubox-cloner` CLI for build/serve/token/export
|
||||
- First-boot provisioning script with partition resize and mesh join
|
||||
- Master-link clone tokens with auto-approve for seamless onboarding
|
||||
- Added `secubox clone` and `secubox master-link` CLI command groups
|
||||
- Full workflow: build image on master → TFTP serve → flash target → auto-join mesh
|
||||
|
||||
- **HAProxy "End of Internet" Default Page** — DONE (2026-02-07)
|
||||
- Cyberpunk fallback page for unknown/unmatched domains
|
||||
|
||||
306
package/secubox/secubox-core/root/etc/uci-defaults/50-secubox-clone-provision
Executable file
306
package/secubox/secubox-core/root/etc/uci-defaults/50-secubox-clone-provision
Executable file
@ -0,0 +1,306 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# SecuBox Clone Auto-Provisioning
|
||||
# Runs on first boot of cloned devices to:
|
||||
# 1. Resize root partition to full disk
|
||||
# 2. Configure as mesh peer
|
||||
# 3. Join mesh with token or request approval
|
||||
#
|
||||
# Environment variables (set by clone builder):
|
||||
# SECUBOX_MASTER - Master IP address
|
||||
# SECUBOX_CLONE_TOKEN - Pre-approved join token
|
||||
# SECUBOX_AUTO_JOIN - Auto-join mesh (1=yes, 0=request only)
|
||||
#
|
||||
|
||||
# Check if we're a clone (marker file or env)
|
||||
CLONE_MARKER="/etc/secubox/.clone-provision"
|
||||
PROVISION_LOG="/var/log/secubox-clone-provision.log"
|
||||
|
||||
# Default values (can be overridden by embedded config)
|
||||
MASTER_IP="${SECUBOX_MASTER:-}"
|
||||
CLONE_TOKEN="${SECUBOX_CLONE_TOKEN:-}"
|
||||
AUTO_JOIN="${SECUBOX_AUTO_JOIN:-1}"
|
||||
|
||||
log() {
|
||||
local msg="[$(date +%T)] $*"
|
||||
echo "$msg" >> "$PROVISION_LOG"
|
||||
logger -t secubox-clone "$*"
|
||||
echo "$msg"
|
||||
}
|
||||
|
||||
# Try to load embedded clone config
|
||||
load_clone_config() {
|
||||
# Check for config in /etc/secubox/clone.conf
|
||||
if [ -f /etc/secubox/clone.conf ]; then
|
||||
. /etc/secubox/clone.conf
|
||||
log "Loaded clone config from /etc/secubox/clone.conf"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for TFTP-downloaded config
|
||||
if [ -f /tmp/clone-config.sh ]; then
|
||||
. /tmp/clone-config.sh
|
||||
log "Loaded clone config from /tmp/clone-config.sh"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Resize root partition to full disk
|
||||
resize_root() {
|
||||
log "Checking root partition resize..."
|
||||
|
||||
ROOT_DEV=$(awk '$2=="/" {print $1}' /proc/mounts)
|
||||
if [ -z "$ROOT_DEV" ]; then
|
||||
log "Cannot determine root device"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine disk and partition number
|
||||
# Handle both /dev/mmcblk0p2 and /dev/sda2 formats
|
||||
if echo "$ROOT_DEV" | grep -q "mmcblk\|nvme"; then
|
||||
# MMC/NVMe style: /dev/mmcblk0p2 -> /dev/mmcblk0 + 2
|
||||
DISK=$(echo "$ROOT_DEV" | sed 's/p[0-9]*$//')
|
||||
PART_NUM=$(echo "$ROOT_DEV" | grep -o 'p[0-9]*$' | tr -d 'p')
|
||||
else
|
||||
# SCSI style: /dev/sda2 -> /dev/sda + 2
|
||||
DISK=$(echo "$ROOT_DEV" | sed 's/[0-9]*$//')
|
||||
PART_NUM=$(echo "$ROOT_DEV" | grep -o '[0-9]*$')
|
||||
fi
|
||||
|
||||
if [ -z "$DISK" ] || [ -z "$PART_NUM" ]; then
|
||||
log "Cannot parse disk/partition from $ROOT_DEV"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Root device: $ROOT_DEV (disk: $DISK, partition: $PART_NUM)"
|
||||
|
||||
# Get current and potential sizes
|
||||
local current_size=$(df -m / | tail -1 | awk '{print $2}')
|
||||
log "Current root size: ${current_size}MB"
|
||||
|
||||
# Check if parted is available
|
||||
if ! command -v parted >/dev/null 2>&1; then
|
||||
log "parted not available, skipping resize"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get disk total size
|
||||
local disk_size=$(parted -s "$DISK" unit MB print 2>/dev/null | \
|
||||
grep "^Disk $DISK" | sed 's/.*: //' | tr -d 'MB')
|
||||
|
||||
if [ -n "$disk_size" ]; then
|
||||
log "Disk size: ${disk_size}MB"
|
||||
|
||||
# Only resize if there's significant space to gain (>10%)
|
||||
local potential_gain=$((disk_size - current_size))
|
||||
if [ "$potential_gain" -gt "$((current_size / 10))" ]; then
|
||||
log "Resizing partition $PART_NUM to 100%..."
|
||||
|
||||
# Resize partition
|
||||
if parted -s "$DISK" resizepart "$PART_NUM" 100% 2>/dev/null; then
|
||||
log "Partition resized"
|
||||
|
||||
# Resize filesystem
|
||||
if resize2fs "$ROOT_DEV" 2>/dev/null; then
|
||||
sync
|
||||
local new_size=$(df -m / | tail -1 | awk '{print $2}')
|
||||
log "Filesystem resized: ${current_size}MB -> ${new_size}MB"
|
||||
else
|
||||
log "resize2fs failed (may need reboot)"
|
||||
fi
|
||||
else
|
||||
log "parted resize failed"
|
||||
fi
|
||||
else
|
||||
log "Partition already at optimal size"
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Discover master via mDNS or network scan
|
||||
discover_master() {
|
||||
# If master already set, use it
|
||||
if [ -n "$MASTER_IP" ]; then
|
||||
log "Master IP from config: $MASTER_IP"
|
||||
echo "$MASTER_IP"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Discovering master..."
|
||||
|
||||
# Try mDNS if avahi is available
|
||||
if command -v avahi-browse >/dev/null 2>&1; then
|
||||
local mdns_master=$(avahi-browse -rt _secubox._tcp 2>/dev/null | \
|
||||
grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
|
||||
if [ -n "$mdns_master" ]; then
|
||||
log "Found master via mDNS: $mdns_master"
|
||||
echo "$mdns_master"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try common gateway (often the master)
|
||||
local gateway=$(ip route | grep default | awk '{print $3}' | head -1)
|
||||
if [ -n "$gateway" ]; then
|
||||
# Check if it responds to SecuBox P2P port
|
||||
if nc -z -w2 "$gateway" 7331 2>/dev/null; then
|
||||
log "Found master at gateway: $gateway"
|
||||
echo "$gateway"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Scan common SecuBox subnets
|
||||
for subnet in 192.168.255 192.168.1 192.168.0; do
|
||||
if nc -z -w1 "${subnet}.1" 7331 2>/dev/null; then
|
||||
log "Found master via scan: ${subnet}.1"
|
||||
echo "${subnet}.1"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
log "No master discovered"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Configure as mesh peer
|
||||
configure_peer() {
|
||||
local master="$1"
|
||||
|
||||
log "Configuring as mesh peer (upstream: $master)..."
|
||||
|
||||
# Set master-link configuration
|
||||
uci -q batch <<-EOF
|
||||
set master-link.main=master-link
|
||||
set master-link.main.enabled='1'
|
||||
set master-link.main.role='peer'
|
||||
set master-link.main.upstream='$master'
|
||||
set master-link.main.auto_approve='0'
|
||||
commit master-link
|
||||
EOF
|
||||
|
||||
# Mark as clone
|
||||
mkdir -p /etc/secubox
|
||||
echo "$master" > /etc/secubox/.clone-master
|
||||
date -Iseconds > /etc/secubox/.clone-provisioned
|
||||
|
||||
log "Peer configuration saved"
|
||||
}
|
||||
|
||||
# Join mesh network
|
||||
join_mesh() {
|
||||
local master="$1"
|
||||
local token="$2"
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
log "Joining mesh with pre-approved token..."
|
||||
|
||||
# Use master-link join with token
|
||||
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||
if /usr/lib/secubox/master-link.sh join "$master" "$token" 2>/dev/null; then
|
||||
log "Joined mesh successfully with token"
|
||||
return 0
|
||||
else
|
||||
log "Token join failed, falling back to request"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Request join (requires manual approval on master)
|
||||
if [ "$AUTO_JOIN" = "1" ]; then
|
||||
log "Requesting mesh join (needs approval on master)..."
|
||||
|
||||
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||
if /usr/lib/secubox/master-link.sh request_join "$master" 2>/dev/null; then
|
||||
log "Join request sent to master"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "Auto-join disabled, manual mesh setup required"
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Start SecuBox services
|
||||
start_services() {
|
||||
log "Starting SecuBox services..."
|
||||
|
||||
# Enable and start core service
|
||||
if [ -x /etc/init.d/secubox-core ]; then
|
||||
/etc/init.d/secubox-core enable
|
||||
/etc/init.d/secubox-core start
|
||||
fi
|
||||
|
||||
# Enable master-link for mesh
|
||||
if [ -x /etc/init.d/secubox-master-link ]; then
|
||||
/etc/init.d/secubox-master-link enable
|
||||
/etc/init.d/secubox-master-link start
|
||||
fi
|
||||
|
||||
log "Services started"
|
||||
}
|
||||
|
||||
# Main provisioning flow
|
||||
main() {
|
||||
log "═══════════════════════════════════════════"
|
||||
log "SecuBox Clone Provisioning Starting"
|
||||
log "═══════════════════════════════════════════"
|
||||
|
||||
# Load any embedded config
|
||||
load_clone_config
|
||||
|
||||
# Step 1: Resize root partition
|
||||
log "Step 1/4: Resize root partition"
|
||||
resize_root || log "Resize skipped or failed"
|
||||
|
||||
# Step 2: Discover master
|
||||
log "Step 2/4: Discover master"
|
||||
MASTER_IP=$(discover_master)
|
||||
if [ -z "$MASTER_IP" ]; then
|
||||
log "No master found - standalone mode"
|
||||
log "Manual mesh setup required: secubox master-link join <master-ip>"
|
||||
fi
|
||||
|
||||
# Step 3: Configure as peer
|
||||
if [ -n "$MASTER_IP" ]; then
|
||||
log "Step 3/4: Configure as mesh peer"
|
||||
configure_peer "$MASTER_IP"
|
||||
|
||||
# Step 4: Join mesh
|
||||
log "Step 4/4: Join mesh"
|
||||
join_mesh "$MASTER_IP" "$CLONE_TOKEN"
|
||||
else
|
||||
log "Step 3/4: Skipped (no master)"
|
||||
log "Step 4/4: Skipped (no master)"
|
||||
fi
|
||||
|
||||
# Start services
|
||||
start_services
|
||||
|
||||
# Summary
|
||||
log "═══════════════════════════════════════════"
|
||||
log "Clone Provisioning Complete"
|
||||
log "───────────────────────────────────────────"
|
||||
log "Root size: $(df -h / | tail -1 | awk '{print $2}')"
|
||||
log "Master: ${MASTER_IP:-none}"
|
||||
log "Mesh status: $(uci -q get master-link.main.role 2>/dev/null || echo 'unconfigured')"
|
||||
log "═══════════════════════════════════════════"
|
||||
|
||||
# Mark provisioning complete (prevents re-run)
|
||||
touch "$CLONE_MARKER"
|
||||
}
|
||||
|
||||
# Run if not already provisioned
|
||||
if [ ! -f "$CLONE_MARKER" ]; then
|
||||
main
|
||||
else
|
||||
logger -t secubox-clone "Clone already provisioned, skipping"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@ -23,27 +23,28 @@ Modular OpenWrt Security Appliance Framework
|
||||
${BOLD}Usage:${NC} secubox <command> [subcommand] [options]
|
||||
|
||||
${BOLD}Commands:${NC}
|
||||
${GREEN}app${NC} Manage modules and AppStore
|
||||
${GREEN}feed${NC} Manage catalog feed sources
|
||||
${GREEN}profile${NC} Manage profiles and templates
|
||||
${GREEN}skill${NC} Discover and manage skills
|
||||
${GREEN}feedback${NC} Report issues and find resolutions
|
||||
${GREEN}device${NC} Device information and management
|
||||
${GREEN}net${NC} Network management
|
||||
${GREEN}diag${NC} Diagnostics and health checks
|
||||
${GREEN}landing${NC} Generate landing pages from vhosts
|
||||
${GREEN}ai${NC} AI copilot (optional)
|
||||
${GREEN}app${NC} Manage modules and AppStore
|
||||
${GREEN}feed${NC} Manage catalog feed sources
|
||||
${GREEN}profile${NC} Manage profiles and templates
|
||||
${GREEN}skill${NC} Discover and manage skills
|
||||
${GREEN}feedback${NC} Report issues and find resolutions
|
||||
${GREEN}device${NC} Device information and management
|
||||
${GREEN}net${NC} Network management
|
||||
${GREEN}diag${NC} Diagnostics and health checks
|
||||
${GREEN}landing${NC} Generate landing pages from vhosts
|
||||
${GREEN}clone${NC} Station cloning and deployment
|
||||
${GREEN}master-link${NC} Mesh network management
|
||||
${GREEN}ai${NC} AI copilot (optional)
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
secubox app list
|
||||
secubox app install wireguard-vpn
|
||||
secubox feed list
|
||||
secubox feed add my-feed http://example.com/catalog.json --type unpublished
|
||||
secubox profile export --name "My Setup" --include-feeds
|
||||
secubox skill list
|
||||
secubox feedback report luci-app-example --type bug --summary "Crash on load"
|
||||
secubox profile export --name "My Setup"
|
||||
secubox diag health
|
||||
secubox device status
|
||||
secubox clone build && secubox clone serve --start
|
||||
secubox master-link status
|
||||
|
||||
Run ${BOLD}secubox <command> help${NC} for command-specific help.
|
||||
EOF
|
||||
@ -464,6 +465,105 @@ EOF
|
||||
esac
|
||||
}
|
||||
|
||||
# Clone commands
|
||||
cmd_clone() {
|
||||
case "$1" in
|
||||
build)
|
||||
shift
|
||||
/usr/sbin/secubox-cloner build "$@"
|
||||
;;
|
||||
serve)
|
||||
shift
|
||||
/usr/sbin/secubox-cloner serve "$@"
|
||||
;;
|
||||
token)
|
||||
shift
|
||||
/usr/sbin/secubox-cloner token "$@"
|
||||
;;
|
||||
status)
|
||||
/usr/sbin/secubox-cloner status
|
||||
;;
|
||||
list)
|
||||
/usr/sbin/secubox-cloner list
|
||||
;;
|
||||
export)
|
||||
shift
|
||||
/usr/sbin/secubox-cloner export "$@"
|
||||
;;
|
||||
help|*)
|
||||
cat <<EOF
|
||||
${BOLD}secubox clone${NC} - Station cloning and deployment
|
||||
|
||||
${BOLD}Usage:${NC}
|
||||
secubox clone build [--resize SIZE] Build clone image for current device
|
||||
secubox clone serve [--start|--stop] Manage TFTP clone server
|
||||
secubox clone token [--auto-approve] Generate clone join token
|
||||
secubox clone status Show cloner status
|
||||
secubox clone list List pending/joined clones
|
||||
secubox clone export [FILE] Export clone image
|
||||
|
||||
${BOLD}Clone Workflow:${NC}
|
||||
1. ${GREEN}secubox clone build${NC} Build clone image for same device type
|
||||
2. ${GREEN}secubox clone serve --start${NC} Start TFTP server
|
||||
3. Boot target from TFTP (see U-Boot commands)
|
||||
4. ${GREEN}secubox clone list${NC} Verify clone joined mesh
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
secubox clone build
|
||||
secubox clone token --auto-approve
|
||||
secubox clone serve --start
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Master-link shortcut commands
|
||||
cmd_master_link() {
|
||||
case "$1" in
|
||||
status)
|
||||
/usr/lib/secubox/master-link.sh status
|
||||
;;
|
||||
peers)
|
||||
/usr/lib/secubox/master-link.sh peers
|
||||
;;
|
||||
token)
|
||||
/usr/lib/secubox/master-link.sh token-generate
|
||||
;;
|
||||
clone-token)
|
||||
/usr/lib/secubox/master-link.sh clone-token
|
||||
;;
|
||||
join)
|
||||
shift
|
||||
/usr/lib/secubox/master-link.sh join "$@"
|
||||
;;
|
||||
approve)
|
||||
/usr/lib/secubox/master-link.sh join-approve "$2"
|
||||
;;
|
||||
pending)
|
||||
ls -la /var/lib/secubox-master-link/requests/ 2>/dev/null || echo "No pending requests"
|
||||
;;
|
||||
help|*)
|
||||
cat <<EOF
|
||||
${BOLD}secubox master-link${NC} - Mesh network management
|
||||
|
||||
${BOLD}Usage:${NC}
|
||||
secubox master-link status Show mesh status
|
||||
secubox master-link peers List mesh peers
|
||||
secubox master-link token Generate join token
|
||||
secubox master-link clone-token Generate auto-approve clone token
|
||||
secubox master-link join <ip> Join a mesh (as peer)
|
||||
secubox master-link approve <fp> Approve pending join request
|
||||
secubox master-link pending List pending join requests
|
||||
|
||||
${BOLD}Examples:${NC}
|
||||
secubox master-link status
|
||||
secubox master-link token
|
||||
secubox master-link approve abc123
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main command router
|
||||
case "$1" in
|
||||
app)
|
||||
@ -506,6 +606,14 @@ case "$1" in
|
||||
shift
|
||||
cmd_ai "$@"
|
||||
;;
|
||||
clone|cloner)
|
||||
shift
|
||||
cmd_clone "$@"
|
||||
;;
|
||||
master-link|mesh)
|
||||
shift
|
||||
cmd_master_link "$@"
|
||||
;;
|
||||
-v|--version|version)
|
||||
echo "SecuBox v${SECUBOX_VERSION}"
|
||||
;;
|
||||
|
||||
730
package/secubox/secubox-core/root/usr/sbin/secubox-cloner
Executable file
730
package/secubox/secubox-core/root/usr/sbin/secubox-cloner
Executable file
@ -0,0 +1,730 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# secubox-cloner - On-device clone image builder and server
|
||||
#
|
||||
# Builds and serves SecuBox clone images for same-device-type deployment.
|
||||
# Cloned devices auto-resize root and join mesh as peers.
|
||||
#
|
||||
# Usage:
|
||||
# secubox-cloner build [--resize SIZE] Build clone image for current device
|
||||
# secubox-cloner serve [--start|--stop] Manage TFTP clone server
|
||||
# secubox-cloner token [--auto-approve] Generate clone join token
|
||||
# secubox-cloner status Show cloner status
|
||||
# secubox-cloner list List pending/joined clones
|
||||
# secubox-cloner export [FILE] Export clone image to file
|
||||
#
|
||||
|
||||
VERSION="1.0.0"
|
||||
|
||||
# Directories
|
||||
CLONE_DIR="/srv/secubox/clone"
|
||||
TFTP_ROOT="/srv/tftp"
|
||||
TOKENS_DIR="/var/run/secubox/clone-tokens"
|
||||
STATE_FILE="/var/run/secubox/cloner.state"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[CLONER]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# ============================================================================
|
||||
# Device Detection
|
||||
# ============================================================================
|
||||
|
||||
detect_device() {
|
||||
local board_name=""
|
||||
|
||||
# Try /tmp/sysinfo/board_name first (most reliable)
|
||||
if [ -f /tmp/sysinfo/board_name ]; then
|
||||
board_name=$(cat /tmp/sysinfo/board_name)
|
||||
fi
|
||||
|
||||
# Fallback to uci
|
||||
if [ -z "$board_name" ]; then
|
||||
board_name=$(uci -q get system.@system[0].hostname 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Map board name to device type
|
||||
case "$board_name" in
|
||||
*mochabin*|*MOCHAbin*|globalscale,mochabin)
|
||||
echo "mochabin"
|
||||
;;
|
||||
*espressobin*ultra*|globalscale,espressobin-ultra)
|
||||
echo "espressobin-ultra"
|
||||
;;
|
||||
*espressobin*|globalscale,espressobin*)
|
||||
echo "espressobin-v7"
|
||||
;;
|
||||
*x86*|*generic*)
|
||||
echo "x86-64"
|
||||
;;
|
||||
*)
|
||||
# Try to detect from kernel
|
||||
if grep -q "Marvell" /proc/cpuinfo 2>/dev/null; then
|
||||
if grep -q "Cortex-A72" /proc/cpuinfo 2>/dev/null; then
|
||||
echo "mochabin"
|
||||
else
|
||||
echo "espressobin-v7"
|
||||
fi
|
||||
elif uname -m | grep -q "x86_64"; then
|
||||
echo "x86-64"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_lan_ip() {
|
||||
uci -q get network.lan.ipaddr 2>/dev/null || echo "192.168.255.1"
|
||||
}
|
||||
|
||||
get_device_id() {
|
||||
uci -q get secubox.main.device_id 2>/dev/null || \
|
||||
(cat /sys/class/net/eth0/address 2>/dev/null | tr -d ':' | sha256sum | cut -d' ' -f1 | cut -c1-16)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Image Building
|
||||
# ============================================================================
|
||||
|
||||
build_image() {
|
||||
local resize_target="${1:-}"
|
||||
|
||||
log "Building clone image..."
|
||||
|
||||
# Detect device type
|
||||
local device_type=$(detect_device)
|
||||
if [ "$device_type" = "unknown" ]; then
|
||||
error "Could not detect device type"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Device type: $device_type"
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$CLONE_DIR"
|
||||
mkdir -p "$TFTP_ROOT"
|
||||
|
||||
# Method 1: Export current firmware (if sysupgrade backup works)
|
||||
# This creates a perfect clone of the running system
|
||||
local image_name="secubox-clone-${device_type}.img"
|
||||
local image_path="$CLONE_DIR/$image_name"
|
||||
|
||||
# Check if we have a pre-built image we can use
|
||||
local existing_image=$(ls -t "$CLONE_DIR"/*.img.gz 2>/dev/null | head -1)
|
||||
if [ -n "$existing_image" ]; then
|
||||
log "Using existing image: $existing_image"
|
||||
image_path="$existing_image"
|
||||
else
|
||||
# Try to build via ASU API if curl is available
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
log "Building image via ASU API..."
|
||||
build_via_asu "$device_type" "$image_path" || {
|
||||
warn "ASU build failed, trying local export..."
|
||||
build_local_export "$image_path"
|
||||
}
|
||||
else
|
||||
build_local_export "$image_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Inject clone provisioning script
|
||||
inject_clone_provision "$image_path"
|
||||
|
||||
# Optionally resize image
|
||||
if [ -n "$resize_target" ]; then
|
||||
resize_image "$image_path" "$resize_target"
|
||||
fi
|
||||
|
||||
# Copy to TFTP root
|
||||
log "Copying to TFTP root..."
|
||||
if [ -f "$image_path" ]; then
|
||||
local tftp_image="$TFTP_ROOT/secubox-clone.img"
|
||||
if echo "$image_path" | grep -q "\.gz$"; then
|
||||
gunzip -c "$image_path" > "$tftp_image"
|
||||
else
|
||||
cp "$image_path" "$tftp_image"
|
||||
fi
|
||||
log "Clone image ready: $tftp_image"
|
||||
log "Size: $(ls -lh "$tftp_image" | awk '{print $5}')"
|
||||
fi
|
||||
|
||||
# Save state
|
||||
cat > "$STATE_FILE" <<EOF
|
||||
DEVICE_TYPE="$device_type"
|
||||
IMAGE_PATH="$image_path"
|
||||
TFTP_IMAGE="$TFTP_ROOT/secubox-clone.img"
|
||||
BUILD_TIME="$(date -Iseconds)"
|
||||
LAN_IP="$(get_lan_ip)"
|
||||
EOF
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
build_via_asu() {
|
||||
local device_type="$1"
|
||||
local output="$2"
|
||||
|
||||
# Map device type to ASU profile
|
||||
local target=""
|
||||
local profile=""
|
||||
|
||||
case "$device_type" in
|
||||
mochabin)
|
||||
target="mvebu/cortexa72"
|
||||
profile="globalscale_mochabin"
|
||||
;;
|
||||
espressobin-v7)
|
||||
target="mvebu/cortexa53"
|
||||
profile="globalscale_espressobin"
|
||||
;;
|
||||
espressobin-ultra)
|
||||
target="mvebu/cortexa53"
|
||||
profile="globalscale_espressobin-ultra"
|
||||
;;
|
||||
x86-64)
|
||||
target="x86/64"
|
||||
profile="generic"
|
||||
;;
|
||||
*)
|
||||
error "Unknown device type: $device_type"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Get installed packages (for cloning same config)
|
||||
local packages=$(opkg list-installed 2>/dev/null | awk '{print $1}' | grep -v "^kernel" | tr '\n' ' ')
|
||||
|
||||
# Minimal clone packages (core system + SecuBox essentials)
|
||||
local clone_packages="luci luci-ssl dnsmasq-full curl wget-ssl ca-certificates"
|
||||
clone_packages="$clone_packages wireguard-tools luci-proto-wireguard"
|
||||
clone_packages="$clone_packages haproxy docker lxc lxc-attach"
|
||||
clone_packages="$clone_packages block-mount e2fsprogs parted"
|
||||
|
||||
# Build request JSON
|
||||
local request_json="/tmp/asu-request.json"
|
||||
cat > "$request_json" <<EOF
|
||||
{
|
||||
"profile": "$profile",
|
||||
"target": "$target",
|
||||
"version": "24.10.5",
|
||||
"packages": "$clone_packages",
|
||||
"rootfs_size_mb": 1024,
|
||||
"client": "secubox-cloner/1.0"
|
||||
}
|
||||
EOF
|
||||
|
||||
log "Submitting ASU build request..."
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "@$request_json" \
|
||||
"https://sysupgrade.openwrt.org/api/v1/build" 2>/dev/null)
|
||||
|
||||
local http_code=$(echo "$response" | tail -1)
|
||||
local body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [ "$http_code" = "200" ] || [ "$http_code" = "202" ]; then
|
||||
# Extract request hash
|
||||
local request_hash=$(echo "$body" | jsonfilter -e '@.request_hash' 2>/dev/null)
|
||||
|
||||
if [ -n "$request_hash" ]; then
|
||||
log "Build queued: $request_hash"
|
||||
log "This may take several minutes..."
|
||||
|
||||
# Poll for completion
|
||||
local max_wait=600
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $max_wait ]; do
|
||||
sleep 10
|
||||
elapsed=$((elapsed + 10))
|
||||
|
||||
response=$(curl -s "https://sysupgrade.openwrt.org/api/v1/build/$request_hash" 2>/dev/null)
|
||||
local images=$(echo "$response" | jsonfilter -e '@.images[*]' 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$images" -gt 0 ]; then
|
||||
# Find ext4-sdcard image
|
||||
local image_name=$(echo "$response" | jsonfilter -e '@.images[*].name' 2>/dev/null | grep -E "ext4.*sdcard" | head -1)
|
||||
if [ -z "$image_name" ]; then
|
||||
image_name=$(echo "$response" | jsonfilter -e '@.images[0].name' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -n "$image_name" ]; then
|
||||
log "Downloading: $image_name"
|
||||
curl -# -o "$output" \
|
||||
"https://sysupgrade.openwrt.org/store/$request_hash/$image_name" || {
|
||||
error "Download failed"
|
||||
return 1
|
||||
}
|
||||
log "Image downloaded: $output"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\r Waiting... %ds " "$elapsed" >&2
|
||||
done
|
||||
|
||||
error "Build timed out"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
error "ASU request failed: HTTP $http_code"
|
||||
return 1
|
||||
}
|
||||
|
||||
build_local_export() {
|
||||
local output="$1"
|
||||
|
||||
log "Creating local system export..."
|
||||
|
||||
# Use sysupgrade backup as base
|
||||
local backup="/tmp/secubox-backup.tar.gz"
|
||||
sysupgrade -b "$backup" 2>/dev/null
|
||||
|
||||
if [ -f "$backup" ]; then
|
||||
log "System backup created: $(ls -lh "$backup" | awk '{print $5}')"
|
||||
# For now, just note that we need a pre-built base image
|
||||
warn "Full image build requires base firmware image"
|
||||
warn "Use ASU API or provide pre-built image in $CLONE_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
resize_image() {
|
||||
local image="$1"
|
||||
local target_size="$2"
|
||||
|
||||
log "Resizing image to $target_size..."
|
||||
|
||||
# Check for resize tools
|
||||
if ! command -v parted >/dev/null 2>&1; then
|
||||
warn "parted not available, skipping resize"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# This would use the resize-openwrt-image.sh approach
|
||||
# For on-device use, we'll defer resize to first boot
|
||||
log "Image will auto-resize to full disk at first boot"
|
||||
return 0
|
||||
}
|
||||
|
||||
inject_clone_provision() {
|
||||
local image="$1"
|
||||
local master_ip=$(get_lan_ip)
|
||||
|
||||
log "Generating clone provision script..."
|
||||
|
||||
# Create provision script for TFTP download at first boot
|
||||
local script="$TFTP_ROOT/clone-provision.sh"
|
||||
cat > "$script" <<PROVISION
|
||||
#!/bin/sh
|
||||
# SecuBox Clone Provision Script
|
||||
# Downloaded and executed at first boot by cloned device
|
||||
|
||||
MASTER_IP="$master_ip"
|
||||
CLONE_TOKEN="${CLONE_TOKEN:-}"
|
||||
|
||||
log() { logger -t secubox-clone "\$*"; echo "\$*"; }
|
||||
|
||||
log "SecuBox clone provisioning starting..."
|
||||
log "Master: \$MASTER_IP"
|
||||
|
||||
# Step 1: Resize root partition to full disk
|
||||
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 || true
|
||||
resize2fs "\$ROOT_DEV" 2>/dev/null || true
|
||||
sync
|
||||
log "Root resized: \$(df -h / | tail -1 | awk '{print \$2}')"
|
||||
else
|
||||
log "parted not available, skipping resize"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: Configure as mesh peer
|
||||
log "Configuring as mesh peer..."
|
||||
uci set master-link.main=master-link
|
||||
uci set master-link.main.role='peer'
|
||||
uci set master-link.main.upstream="\$MASTER_IP"
|
||||
uci commit master-link
|
||||
|
||||
# Step 3: Join mesh
|
||||
if [ -n "\$CLONE_TOKEN" ]; then
|
||||
log "Joining mesh with pre-approved token..."
|
||||
/usr/lib/secubox/master-link.sh join "\$MASTER_IP" "\$CLONE_TOKEN" 2>/dev/null || {
|
||||
log "Token join failed, requesting approval..."
|
||||
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
|
||||
}
|
||||
else
|
||||
log "No token provided, requesting mesh join (needs approval)..."
|
||||
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Step 4: Start SecuBox services
|
||||
log "Starting SecuBox services..."
|
||||
/etc/init.d/secubox-core enable 2>/dev/null
|
||||
/etc/init.d/secubox-core start 2>/dev/null
|
||||
|
||||
log "Clone provisioning complete"
|
||||
PROVISION
|
||||
|
||||
chmod +x "$script"
|
||||
log "Clone provision script: $script"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# TFTP Server
|
||||
# ============================================================================
|
||||
|
||||
serve_start() {
|
||||
log "Starting clone TFTP server..."
|
||||
|
||||
# Use existing TFTP recovery infrastructure
|
||||
if [ -x /usr/sbin/secubox-tftp-recovery ]; then
|
||||
/usr/sbin/secubox-tftp-recovery start
|
||||
else
|
||||
# Manual dnsmasq TFTP setup
|
||||
mkdir -p "$TFTP_ROOT"
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
# Generate U-Boot script for clone
|
||||
generate_uboot_script
|
||||
|
||||
local lan_ip=$(get_lan_ip)
|
||||
log "TFTP server running"
|
||||
log "Clone image: $TFTP_ROOT/secubox-clone.img"
|
||||
log "Server IP: $lan_ip:69"
|
||||
|
||||
echo ""
|
||||
echo "To flash a target device, use these U-Boot commands:"
|
||||
echo " setenv serverip $lan_ip"
|
||||
echo " dhcp"
|
||||
echo " tftpboot 0x20000000 secubox-clone.img"
|
||||
echo " mmc dev 0"
|
||||
echo ' mmc write 0x20000000 0 ${filesize}'
|
||||
echo " reset"
|
||||
}
|
||||
|
||||
serve_stop() {
|
||||
log "Stopping clone TFTP server..."
|
||||
|
||||
if [ -x /usr/sbin/secubox-tftp-recovery ]; then
|
||||
/usr/sbin/secubox-tftp-recovery stop
|
||||
else
|
||||
uci -q set dhcp.@dnsmasq[0].enable_tftp='0'
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
fi
|
||||
|
||||
log "TFTP server stopped"
|
||||
}
|
||||
|
||||
generate_uboot_script() {
|
||||
local lan_ip=$(get_lan_ip)
|
||||
local script_dir="$TFTP_ROOT"
|
||||
|
||||
# Generate human-readable U-Boot commands
|
||||
cat > "$script_dir/clone-uboot.txt" <<UBOOT
|
||||
# SecuBox Clone Boot Commands
|
||||
# Run these in U-Boot to flash the clone image
|
||||
|
||||
# 1. Set network
|
||||
setenv serverip $lan_ip
|
||||
setenv ipaddr 192.168.1.100
|
||||
|
||||
# 2. Get IP via DHCP (optional)
|
||||
dhcp
|
||||
|
||||
# 3. Download clone image
|
||||
tftpboot 0x20000000 secubox-clone.img
|
||||
|
||||
# 4. Write to eMMC/SD
|
||||
mmc dev 0
|
||||
mmc write 0x20000000 0 \${filesize}
|
||||
|
||||
# 5. Boot cloned system
|
||||
reset
|
||||
UBOOT
|
||||
|
||||
log "U-Boot commands: $script_dir/clone-uboot.txt"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Token Management
|
||||
# ============================================================================
|
||||
|
||||
generate_token() {
|
||||
local auto_approve="${1:-0}"
|
||||
local ttl="${2:-86400}" # 24 hours default
|
||||
|
||||
mkdir -p "$TOKENS_DIR"
|
||||
|
||||
# Generate random token
|
||||
local token=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1)
|
||||
|
||||
# Save token metadata
|
||||
local token_file="$TOKENS_DIR/${token}.json"
|
||||
cat > "$token_file" <<EOF
|
||||
{
|
||||
"token": "$token",
|
||||
"created": "$(date -Iseconds)",
|
||||
"expires": "$(date -d "+${ttl} seconds" -Iseconds 2>/dev/null || date -Iseconds)",
|
||||
"ttl": $ttl,
|
||||
"auto_approve": $([ "$auto_approve" = "1" ] && echo "true" || echo "false"),
|
||||
"type": "clone",
|
||||
"used": false
|
||||
}
|
||||
EOF
|
||||
|
||||
# Also register with master-link if available
|
||||
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||
/usr/lib/secubox/master-link.sh register-token "$token" "$ttl" "clone" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Export for use in provision script
|
||||
export CLONE_TOKEN="$token"
|
||||
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Status & Listing
|
||||
# ============================================================================
|
||||
|
||||
show_status() {
|
||||
echo ""
|
||||
echo -e "${BOLD}SecuBox Cloner v$VERSION${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Device info
|
||||
local device_type=$(detect_device)
|
||||
local device_id=$(get_device_id)
|
||||
local lan_ip=$(get_lan_ip)
|
||||
|
||||
echo "Device: $device_type"
|
||||
echo "Device ID: ${device_id:0:16}..."
|
||||
echo "LAN IP: $lan_ip"
|
||||
echo ""
|
||||
|
||||
# Build state
|
||||
echo "Clone Images:"
|
||||
if [ -d "$CLONE_DIR" ]; then
|
||||
for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz 2>/dev/null; do
|
||||
[ -f "$img" ] || continue
|
||||
local size=$(ls -lh "$img" 2>/dev/null | awk '{print $5}')
|
||||
echo " - $(basename "$img") ($size)"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
|
||||
local size=$(ls -lh "$TFTP_ROOT/secubox-clone.img" | awk '{print $5}')
|
||||
echo " - TFTP: secubox-clone.img ($size)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# TFTP status
|
||||
local tftp_enabled=$(uci -q get dhcp.@dnsmasq[0].enable_tftp)
|
||||
if [ "$tftp_enabled" = "1" ]; then
|
||||
echo -e "TFTP: ${GREEN}Running${NC} ($lan_ip:69)"
|
||||
else
|
||||
echo -e "TFTP: ${YELLOW}Stopped${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Pending tokens
|
||||
echo "Clone Tokens:"
|
||||
local token_count=0
|
||||
if [ -d "$TOKENS_DIR" ]; then
|
||||
for tf in "$TOKENS_DIR"/*.json 2>/dev/null; do
|
||||
[ -f "$tf" ] || continue
|
||||
local token=$(jsonfilter -i "$tf" -e '@.token' 2>/dev/null | cut -c1-16)
|
||||
local used=$(jsonfilter -i "$tf" -e '@.used' 2>/dev/null)
|
||||
local auto=$(jsonfilter -i "$tf" -e '@.auto_approve' 2>/dev/null)
|
||||
if [ "$used" = "true" ]; then
|
||||
echo " - ${token}... (used)"
|
||||
elif [ "$auto" = "true" ]; then
|
||||
echo " - ${token}... (auto-approve)"
|
||||
else
|
||||
echo " - ${token}... (pending)"
|
||||
fi
|
||||
token_count=$((token_count + 1))
|
||||
done
|
||||
fi
|
||||
[ $token_count -eq 0 ] && echo " (none)"
|
||||
echo ""
|
||||
|
||||
# Mesh peers (clones)
|
||||
echo "Joined Clones:"
|
||||
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||
/usr/lib/secubox/master-link.sh peers 2>/dev/null | head -5 || echo " (none)"
|
||||
else
|
||||
echo " (master-link not available)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
list_clones() {
|
||||
echo ""
|
||||
echo -e "${BOLD}Cloned Devices${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# List from master-link
|
||||
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||
echo ""
|
||||
echo "Active Peers:"
|
||||
/usr/lib/secubox/master-link.sh peers 2>/dev/null || echo " (none)"
|
||||
|
||||
echo ""
|
||||
echo "Pending Approvals:"
|
||||
/usr/lib/secubox/master-link.sh pending 2>/dev/null || echo " (none)"
|
||||
else
|
||||
echo "master-link not available"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
export_image() {
|
||||
local output="${1:-/tmp/secubox-clone-$(date +%Y%m%d).img.gz}"
|
||||
|
||||
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
|
||||
log "Exporting clone image..."
|
||||
gzip -c "$TFTP_ROOT/secubox-clone.img" > "$output"
|
||||
log "Exported: $output"
|
||||
log "Size: $(ls -lh "$output" | awk '{print $5}')"
|
||||
elif [ -d "$CLONE_DIR" ]; then
|
||||
local latest=$(ls -t "$CLONE_DIR"/*.img* 2>/dev/null | head -1)
|
||||
if [ -n "$latest" ]; then
|
||||
log "Copying: $latest"
|
||||
cp "$latest" "$output"
|
||||
log "Exported: $output"
|
||||
else
|
||||
error "No clone image available. Run: secubox-cloner build"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
error "No clone image available. Run: secubox-cloner build"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Usage
|
||||
# ============================================================================
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
SecuBox Cloner - On-device clone image builder and server
|
||||
|
||||
Usage: secubox-cloner <command> [options]
|
||||
|
||||
Commands:
|
||||
build [--resize SIZE] Build clone image for current device type
|
||||
serve [--start|--stop] Manage TFTP clone server
|
||||
token [--auto-approve] Generate clone join token (24h TTL)
|
||||
status Show cloner status (images, tokens, clones)
|
||||
list List pending and joined clones
|
||||
export [FILE] Export clone image to file
|
||||
|
||||
Options:
|
||||
--resize SIZE Pre-resize image (e.g., 16G for eMMC)
|
||||
--auto-approve Token auto-approves clone join (no manual step)
|
||||
|
||||
Examples:
|
||||
# Build and serve clone image
|
||||
secubox-cloner build
|
||||
secubox-cloner serve --start
|
||||
|
||||
# Generate auto-approve token
|
||||
secubox-cloner token --auto-approve
|
||||
|
||||
# Check status
|
||||
secubox-cloner status
|
||||
|
||||
# Export for USB transfer
|
||||
secubox-cloner export /mnt/usb/clone.img.gz
|
||||
|
||||
Clone Workflow:
|
||||
1. Master: secubox-cloner build && secubox-cloner serve --start
|
||||
2. Target: Boot via TFTP (U-Boot commands shown after serve)
|
||||
3. Target auto-resizes root and joins mesh
|
||||
4. Master: secubox-cloner list (shows new clone)
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
case "${1:-}" in
|
||||
build)
|
||||
shift
|
||||
resize=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--resize) resize="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
build_image "$resize"
|
||||
;;
|
||||
|
||||
serve)
|
||||
case "${2:-start}" in
|
||||
--start|start) serve_start ;;
|
||||
--stop|stop) serve_stop ;;
|
||||
*) serve_start ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
token)
|
||||
auto_approve=0
|
||||
[ "$2" = "--auto-approve" ] && auto_approve=1
|
||||
generate_token "$auto_approve"
|
||||
;;
|
||||
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
|
||||
list)
|
||||
list_clones
|
||||
;;
|
||||
|
||||
export)
|
||||
shift
|
||||
export_image "$@"
|
||||
;;
|
||||
|
||||
help|--help|-h|"")
|
||||
usage
|
||||
;;
|
||||
|
||||
*)
|
||||
error "Unknown command: $1"
|
||||
echo ""
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -194,6 +194,89 @@ ml_token_cleanup() {
|
||||
echo "{\"cleaned\":$cleaned}"
|
||||
}
|
||||
|
||||
# Generate clone-specific token with auto-approve
|
||||
# Used by secubox-cloner for station cloning
|
||||
ml_clone_token_generate() {
|
||||
ml_init
|
||||
|
||||
local ttl="${1:-86400}" # 24 hours default for clones
|
||||
local token_type="${2:-clone}"
|
||||
|
||||
local now=$(date +%s)
|
||||
local expires=$((now + ttl))
|
||||
local rand=$(head -c 32 /dev/urandom 2>/dev/null | sha256sum | cut -d' ' -f1)
|
||||
[ -z "$rand" ] && rand=$(date +%s%N | sha256sum | cut -d' ' -f1)
|
||||
|
||||
# HMAC token using master key
|
||||
local key_data=$(cat "$KEYFILE" 2>/dev/null)
|
||||
local token=$(echo "${key_data}:clone:${rand}:${now}" | sha256sum | cut -d' ' -f1)
|
||||
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
||||
|
||||
# Store token in UCI with auto_approve flag
|
||||
local section_id="clone_$(echo "$token_hash" | cut -c1-8)"
|
||||
uci -q batch <<-EOF
|
||||
set master-link.${section_id}=token
|
||||
set master-link.${section_id}.hash='${token_hash}'
|
||||
set master-link.${section_id}.created='${now}'
|
||||
set master-link.${section_id}.expires='${expires}'
|
||||
set master-link.${section_id}.peer_fp=''
|
||||
set master-link.${section_id}.status='active'
|
||||
set master-link.${section_id}.type='${token_type}'
|
||||
set master-link.${section_id}.auto_approve='1'
|
||||
EOF
|
||||
uci commit master-link
|
||||
|
||||
# Store full token locally for validation
|
||||
echo "$token" > "$ML_TOKENS_DIR/${token_hash}"
|
||||
|
||||
# Record in blockchain
|
||||
local fp=$(factory_fingerprint 2>/dev/null)
|
||||
chain_add_block "clone_token_generated" \
|
||||
"{\"token_hash\":\"$token_hash\",\"type\":\"$token_type\",\"expires\":$expires,\"created_by\":\"$fp\"}" \
|
||||
"$(echo "clone_token:${token_hash}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1
|
||||
|
||||
# Build join URL
|
||||
local my_addr=$(uci -q get network.lan.ipaddr)
|
||||
[ -z "$my_addr" ] && my_addr=$(ip -4 addr show br-lan 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1)
|
||||
|
||||
logger -t master-link "Clone token generated: ${token_hash} (auto-approve, expires: $(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
|
||||
|
||||
cat <<-EOF
|
||||
{
|
||||
"token": "$token",
|
||||
"token_hash": "$token_hash",
|
||||
"type": "$token_type",
|
||||
"auto_approve": true,
|
||||
"expires": $expires,
|
||||
"ttl": $ttl,
|
||||
"master_ip": "$my_addr",
|
||||
"url": "http://${my_addr}:${MESH_PORT}/master-link/?token=${token}&type=clone"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Check if token is a clone token with auto-approve
|
||||
ml_token_is_auto_approve() {
|
||||
local token="$1"
|
||||
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
||||
|
||||
# Find token in UCI
|
||||
local sections=$(uci -q show master-link 2>/dev/null | grep "=token$" | sed "s/master-link\.\(.*\)=token/\1/")
|
||||
for sec in $sections; do
|
||||
local hash=$(uci -q get "master-link.${sec}.hash")
|
||||
if [ "$hash" = "$token_hash" ]; then
|
||||
local auto=$(uci -q get "master-link.${sec}.auto_approve")
|
||||
if [ "$auto" = "1" ]; then
|
||||
echo "true"
|
||||
return 0
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo "false"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Join Protocol
|
||||
# ============================================================================
|
||||
@ -236,9 +319,12 @@ ml_join_request() {
|
||||
|
||||
logger -t master-link "Join request from $peer_hostname ($peer_fp) at $peer_addr"
|
||||
|
||||
# Check auto-approve
|
||||
# Check auto-approve: either global setting or token-specific (clone tokens)
|
||||
local auto_approve=$(uci -q get master-link.main.auto_approve)
|
||||
if [ "$auto_approve" = "1" ]; then
|
||||
local token_auto=$(ml_token_is_auto_approve "$token")
|
||||
|
||||
if [ "$auto_approve" = "1" ] || [ "$token_auto" = "true" ]; then
|
||||
logger -t master-link "Auto-approving join for $peer_fp (global=$auto_approve, token=$token_auto)"
|
||||
ml_join_approve "$peer_fp"
|
||||
return $?
|
||||
fi
|
||||
@ -871,6 +957,37 @@ case "${1:-}" in
|
||||
token-cleanup)
|
||||
ml_token_cleanup
|
||||
;;
|
||||
clone-token|generate-clone-token)
|
||||
# Generate clone-specific auto-approve token
|
||||
# Usage: master-link.sh clone-token [ttl]
|
||||
ml_clone_token_generate "${2:-86400}" "clone"
|
||||
;;
|
||||
register-token)
|
||||
# Register external token (for secubox-cloner integration)
|
||||
# Usage: master-link.sh register-token <token> <ttl> <type>
|
||||
local ext_token="$2"
|
||||
local ext_ttl="${3:-86400}"
|
||||
local ext_type="${4:-clone}"
|
||||
local token_hash=$(echo "$ext_token" | sha256sum | cut -d' ' -f1)
|
||||
local now=$(date +%s)
|
||||
local expires=$((now + ext_ttl))
|
||||
local section_id="ext_$(echo "$token_hash" | cut -c1-8)"
|
||||
uci -q batch <<-EOF
|
||||
set master-link.${section_id}=token
|
||||
set master-link.${section_id}.hash='${token_hash}'
|
||||
set master-link.${section_id}.created='${now}'
|
||||
set master-link.${section_id}.expires='${expires}'
|
||||
set master-link.${section_id}.peer_fp=''
|
||||
set master-link.${section_id}.status='active'
|
||||
set master-link.${section_id}.type='${ext_type}'
|
||||
set master-link.${section_id}.auto_approve='1'
|
||||
EOF
|
||||
uci commit master-link
|
||||
mkdir -p "$ML_TOKENS_DIR"
|
||||
echo "$ext_token" > "$ML_TOKENS_DIR/${token_hash}"
|
||||
logger -t master-link "External token registered: ${token_hash} (type=$ext_type, expires=$(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
|
||||
echo "{\"registered\":true,\"token_hash\":\"$token_hash\",\"expires\":$expires}"
|
||||
;;
|
||||
join-request)
|
||||
ml_join_request "$2" "$3" "$4" "$5"
|
||||
;;
|
||||
|
||||
943
secubox-tools/secubox-clone-station.sh
Executable file
943
secubox-tools/secubox-clone-station.sh
Executable file
@ -0,0 +1,943 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# secubox-clone-station.sh - Host-side SecuBox Station Cloner
|
||||
#
|
||||
# Orchestrates cloning of SecuBox devices via dual USB serial:
|
||||
# - Master device: Extract config, build clone image, generate join token
|
||||
# - Target device: Enter U-Boot, flash image, auto-join mesh
|
||||
#
|
||||
# Dependencies:
|
||||
# - MOKATOOL (mochabin_tool.py) for serial console automation
|
||||
# - secubox-image.sh for ASU API firmware building
|
||||
# - TFTP server (dnsmasq or tftpd-hpa)
|
||||
#
|
||||
# Usage:
|
||||
# ./secubox-clone-station.sh detect # Detect serial devices
|
||||
# ./secubox-clone-station.sh pull [--master DEV] # Pull image from master
|
||||
# ./secubox-clone-station.sh flash [--target DEV] # Flash image to target
|
||||
# ./secubox-clone-station.sh clone # Full workflow
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SECUBOX_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# MOKATOOL location
|
||||
MOKATOOL_DIR="${MOKATOOL_DIR:-/home/reepost/DEVEL/MOKATOOL}"
|
||||
MOKATOOL="$MOKATOOL_DIR/mochabin_tool.py"
|
||||
|
||||
# Clone station directories
|
||||
CLONE_DIR="$SCRIPT_DIR/clone-station"
|
||||
CLONE_IMAGES="$CLONE_DIR/images"
|
||||
CLONE_LOGS="$CLONE_DIR/logs"
|
||||
TFTP_ROOT="${TFTP_ROOT:-/srv/tftp}"
|
||||
|
||||
# Defaults
|
||||
BAUDRATE=115200
|
||||
DEFAULT_MASTER=""
|
||||
DEFAULT_TARGET=""
|
||||
MASTER_DEV=""
|
||||
TARGET_DEV=""
|
||||
OPENWRT_VERSION="24.10.5"
|
||||
CLONE_TOKEN=""
|
||||
|
||||
# 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} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||
|
||||
# =============================================================================
|
||||
# Helpers
|
||||
# =============================================================================
|
||||
|
||||
check_deps() {
|
||||
local missing=0
|
||||
|
||||
# Check MOKATOOL
|
||||
if [[ ! -x "$MOKATOOL" ]]; then
|
||||
log_error "MOKATOOL not found: $MOKATOOL"
|
||||
log_info "Set MOKATOOL_DIR environment variable or install from ~/DEVEL/MOKATOOL"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
# Check Python dependencies
|
||||
if ! python3 -c "import serial, pexpect, rich, typer" 2>/dev/null; then
|
||||
log_warn "Missing Python deps. Install: pip install 'typer[all]' pyserial pexpect rich pyyaml"
|
||||
fi
|
||||
|
||||
# Check TFTP directory
|
||||
if [[ ! -d "$TFTP_ROOT" ]]; then
|
||||
log_warn "TFTP root not found: $TFTP_ROOT"
|
||||
log_info "Create with: sudo mkdir -p $TFTP_ROOT && sudo chmod 777 $TFTP_ROOT"
|
||||
fi
|
||||
|
||||
return $missing
|
||||
}
|
||||
|
||||
mokatool() {
|
||||
python3 "$MOKATOOL" "$@"
|
||||
}
|
||||
|
||||
# Create directory structure
|
||||
init_dirs() {
|
||||
mkdir -p "$CLONE_IMAGES"
|
||||
mkdir -p "$CLONE_LOGS"
|
||||
mkdir -p "$TFTP_ROOT" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get timestamp for logging
|
||||
get_tag() {
|
||||
date +%Y%m%d-%H%M%S
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Device Detection
|
||||
# =============================================================================
|
||||
|
||||
detect_devices() {
|
||||
log_step "Detecting USB serial devices..."
|
||||
|
||||
local found_master=""
|
||||
local found_target=""
|
||||
|
||||
# Use MOKATOOL to list ports
|
||||
mokatool list-ports 2>/dev/null || true
|
||||
echo ""
|
||||
|
||||
# Scan each USB serial device
|
||||
for dev in /dev/ttyUSB* /dev/ttyACM*; do
|
||||
[[ -c "$dev" ]] || continue
|
||||
|
||||
log_info "Probing $dev..."
|
||||
|
||||
# Try to detect device type by sending CR and checking response
|
||||
local response=""
|
||||
response=$(timeout 3 python3 -c "
|
||||
import serial
|
||||
import time
|
||||
try:
|
||||
ser = serial.Serial('$dev', $BAUDRATE, timeout=1)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'\\r\\n')
|
||||
time.sleep(0.5)
|
||||
data = ser.read(1000).decode('utf-8', errors='ignore')
|
||||
print(data)
|
||||
ser.close()
|
||||
except Exception as e:
|
||||
print(f'ERROR: {e}')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$response" | grep -qiE "(SecuBox|OpenWrt|root@|BusyBox)"; then
|
||||
log_info " → ${GREEN}MASTER${NC}: $dev (SecuBox/OpenWrt running)"
|
||||
found_master="$dev"
|
||||
MASTER_DEV="$dev"
|
||||
elif echo "$response" | grep -qiE "(U-Boot|=>|Marvell|Hit any key)"; then
|
||||
log_info " → ${YELLOW}TARGET${NC}: $dev (U-Boot prompt detected)"
|
||||
found_target="$dev"
|
||||
TARGET_DEV="$dev"
|
||||
elif echo "$response" | grep -qiE "login:"; then
|
||||
log_info " → ${GREEN}MASTER${NC}: $dev (Linux login prompt)"
|
||||
found_master="$dev"
|
||||
MASTER_DEV="$dev"
|
||||
else
|
||||
log_info " → Unknown/no response"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Detection Summary:${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
if [[ -n "$MASTER_DEV" ]]; then
|
||||
echo -e " Master: ${GREEN}$MASTER_DEV${NC}"
|
||||
else
|
||||
echo -e " Master: ${RED}Not found${NC}"
|
||||
fi
|
||||
if [[ -n "$TARGET_DEV" ]]; then
|
||||
echo -e " Target: ${GREEN}$TARGET_DEV${NC}"
|
||||
else
|
||||
echo -e " Target: ${YELLOW}Not found (or not at U-Boot)${NC}"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Save to state file
|
||||
cat > "$CLONE_DIR/.detected" <<EOF
|
||||
MASTER_DEV="$MASTER_DEV"
|
||||
TARGET_DEV="$TARGET_DEV"
|
||||
DETECTED_AT="$(date -Iseconds)"
|
||||
EOF
|
||||
}
|
||||
|
||||
load_detected() {
|
||||
if [[ -f "$CLONE_DIR/.detected" ]]; then
|
||||
source "$CLONE_DIR/.detected"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Master Operations
|
||||
# =============================================================================
|
||||
|
||||
get_master_info() {
|
||||
local master="${1:-$MASTER_DEV}"
|
||||
[[ -z "$master" ]] && { log_error "No master device"; return 1; }
|
||||
|
||||
log_step "Getting master device info..."
|
||||
|
||||
# Use Python to send commands and get output
|
||||
python3 -c "
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
|
||||
dev = '$master'
|
||||
try:
|
||||
ser = serial.Serial(dev, $BAUDRATE, timeout=2)
|
||||
time.sleep(0.2)
|
||||
|
||||
# Get board info
|
||||
ser.write(b'cat /tmp/sysinfo/board_name 2>/dev/null || echo unknown\\n')
|
||||
time.sleep(0.3)
|
||||
board = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
|
||||
|
||||
# Get SecuBox version
|
||||
ser.write(b'secubox --version 2>/dev/null || echo unknown\\n')
|
||||
time.sleep(0.3)
|
||||
version = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
|
||||
|
||||
# Get device ID
|
||||
ser.write(b'uci -q get secubox.main.device_id 2>/dev/null || echo unknown\\n')
|
||||
time.sleep(0.3)
|
||||
device_id = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
|
||||
|
||||
# Get LAN IP
|
||||
ser.write(b'uci -q get network.lan.ipaddr 2>/dev/null || echo 192.168.1.1\\n')
|
||||
time.sleep(0.3)
|
||||
lan_ip = ser.read(500).decode('utf-8', errors='ignore').strip().split('\\n')[-1]
|
||||
|
||||
ser.close()
|
||||
|
||||
print(f'Board: {board}')
|
||||
print(f'SecuBox: {version}')
|
||||
print(f'Device ID: {device_id}')
|
||||
print(f'LAN IP: {lan_ip}')
|
||||
except Exception as e:
|
||||
print(f'Error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
" 2>&1
|
||||
}
|
||||
|
||||
generate_clone_token() {
|
||||
local master="${1:-$MASTER_DEV}"
|
||||
[[ -z "$master" ]] && { log_error "No master device"; return 1; }
|
||||
|
||||
log_step "Generating clone token on master..."
|
||||
|
||||
# Send command to generate token
|
||||
CLONE_TOKEN=$(python3 -c "
|
||||
import serial
|
||||
import time
|
||||
import sys
|
||||
|
||||
dev = '$master'
|
||||
try:
|
||||
ser = serial.Serial(dev, $BAUDRATE, timeout=5)
|
||||
time.sleep(0.2)
|
||||
|
||||
# Generate auto-approve clone token (valid 24h)
|
||||
ser.write(b'secubox-cloner token --auto-approve 2>/dev/null || /usr/lib/secubox/master-link.sh generate-token 86400 clone\\n')
|
||||
time.sleep(1)
|
||||
output = ser.read(2000).decode('utf-8', errors='ignore')
|
||||
|
||||
# Extract token (should be a hex string)
|
||||
for line in output.strip().split('\\n'):
|
||||
line = line.strip()
|
||||
if len(line) == 64 and all(c in '0123456789abcdef' for c in line):
|
||||
print(line)
|
||||
break
|
||||
|
||||
ser.close()
|
||||
except Exception as e:
|
||||
print(f'Error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
" 2>&1)
|
||||
|
||||
if [[ -n "$CLONE_TOKEN" && ${#CLONE_TOKEN} -eq 64 ]]; then
|
||||
log_info "Clone token: ${CLONE_TOKEN:0:16}...${CLONE_TOKEN: -8}"
|
||||
echo "$CLONE_TOKEN" > "$CLONE_DIR/.clone_token"
|
||||
return 0
|
||||
else
|
||||
log_warn "Could not generate clone token on master"
|
||||
log_info "Token will need to be generated manually or clone will request approval"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Image Building
|
||||
# =============================================================================
|
||||
|
||||
build_clone_image() {
|
||||
local device="${1:-mochabin}"
|
||||
|
||||
log_step "Building clone image for $device via ASU API..."
|
||||
|
||||
# Use existing secubox-image.sh
|
||||
local image_script="$SCRIPT_DIR/secubox-image.sh"
|
||||
if [[ ! -x "$image_script" ]]; then
|
||||
log_error "secubox-image.sh not found: $image_script"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Build with ext4 (needed for resize)
|
||||
mkdir -p "$CLONE_IMAGES"
|
||||
|
||||
local image_file
|
||||
image_file=$("$image_script" --output "$CLONE_IMAGES" build "$device") || {
|
||||
log_error "Image build failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_info "Clone image built: $image_file"
|
||||
echo "$image_file"
|
||||
}
|
||||
|
||||
inject_clone_config() {
|
||||
local image="$1"
|
||||
local master_ip="$2"
|
||||
local token="${3:-}"
|
||||
|
||||
log_step "Injecting clone configuration into image..."
|
||||
|
||||
# This would require mounting the image and modifying it
|
||||
# For now, we'll create a companion script that gets downloaded at first boot
|
||||
|
||||
local clone_script="$CLONE_DIR/clone-provision.sh"
|
||||
cat > "$clone_script" <<CLONESCRIPT
|
||||
#!/bin/sh
|
||||
# SecuBox Clone Auto-Provision Script
|
||||
# Downloaded and executed at first boot
|
||||
|
||||
MASTER_IP="$master_ip"
|
||||
CLONE_TOKEN="$token"
|
||||
|
||||
log() { logger -t secubox-clone "\$*"; echo "\$*"; }
|
||||
|
||||
log "SecuBox clone provisioning starting..."
|
||||
log "Master: \$MASTER_IP"
|
||||
|
||||
# Step 1: Resize root partition
|
||||
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 || true
|
||||
resize2fs "\$ROOT_DEV" 2>/dev/null || true
|
||||
log "Root resized: \$(df -h / | tail -1 | awk '{print \$2}')"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: Configure as mesh slave
|
||||
log "Configuring as mesh peer..."
|
||||
uci set master-link.main=master-link
|
||||
uci set master-link.main.role='peer'
|
||||
uci set master-link.main.upstream="\$MASTER_IP"
|
||||
uci commit master-link
|
||||
|
||||
# Step 3: Join mesh
|
||||
if [ -n "\$CLONE_TOKEN" ]; then
|
||||
log "Joining mesh with pre-approved token..."
|
||||
/usr/lib/secubox/master-link.sh join "\$MASTER_IP" "\$CLONE_TOKEN" 2>/dev/null || {
|
||||
log "Join with token failed, requesting approval..."
|
||||
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
|
||||
}
|
||||
else
|
||||
log "Requesting mesh join (manual approval required)..."
|
||||
/usr/lib/secubox/master-link.sh request_join "\$MASTER_IP" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "Clone provisioning complete"
|
||||
CLONESCRIPT
|
||||
|
||||
chmod +x "$clone_script"
|
||||
log_info "Clone provision script: $clone_script"
|
||||
|
||||
# Copy to TFTP for first-boot download
|
||||
if [[ -d "$TFTP_ROOT" ]]; then
|
||||
cp "$clone_script" "$TFTP_ROOT/clone-provision.sh"
|
||||
log_info "Script available via TFTP: clone-provision.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Pull from Master
|
||||
# =============================================================================
|
||||
|
||||
cmd_pull() {
|
||||
local master="${MASTER_DEV:-}"
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--master) master="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
load_detected
|
||||
[[ -z "$master" ]] && master="$MASTER_DEV"
|
||||
[[ -z "$master" ]] && { log_error "No master device. Run: $0 detect"; return 1; }
|
||||
|
||||
init_dirs
|
||||
local tag=$(get_tag)
|
||||
|
||||
echo -e "${BOLD}SecuBox Clone Station - Pull from Master${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Master: $master"
|
||||
echo "Tag: $tag"
|
||||
echo ""
|
||||
|
||||
# Get master info
|
||||
get_master_info "$master"
|
||||
echo ""
|
||||
|
||||
# Detect device type from master
|
||||
local board_name
|
||||
board_name=$(python3 -c "
|
||||
import serial
|
||||
import time
|
||||
ser = serial.Serial('$master', $BAUDRATE, timeout=2)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'cat /tmp/sysinfo/board_name 2>/dev/null\\n')
|
||||
time.sleep(0.3)
|
||||
output = ser.read(500).decode('utf-8', errors='ignore')
|
||||
for line in output.strip().split('\\n'):
|
||||
if 'mochabin' in line.lower() or 'espressobin' in line.lower() or 'x86' in line.lower():
|
||||
print(line.strip())
|
||||
break
|
||||
ser.close()
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
local device_type="mochabin" # default
|
||||
case "$board_name" in
|
||||
*mochabin*|*MOCHAbin*) device_type="mochabin" ;;
|
||||
*espressobin*ultra*) device_type="espressobin-ultra" ;;
|
||||
*espressobin*) device_type="espressobin-v7" ;;
|
||||
*x86*|*generic*) device_type="x86-64" ;;
|
||||
esac
|
||||
|
||||
log_info "Detected device type: $device_type"
|
||||
|
||||
# Get master LAN IP
|
||||
local master_ip
|
||||
master_ip=$(python3 -c "
|
||||
import serial
|
||||
import time
|
||||
ser = serial.Serial('$master', $BAUDRATE, timeout=2)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'uci -q get network.lan.ipaddr\\n')
|
||||
time.sleep(0.3)
|
||||
output = ser.read(500).decode('utf-8', errors='ignore')
|
||||
for line in output.strip().split('\\n'):
|
||||
if '.' in line and not line.startswith('uci'):
|
||||
print(line.strip())
|
||||
break
|
||||
ser.close()
|
||||
" 2>/dev/null || echo "192.168.255.1")
|
||||
|
||||
log_info "Master LAN IP: $master_ip"
|
||||
|
||||
# Generate clone token
|
||||
generate_clone_token "$master" || true
|
||||
|
||||
# Build clone image
|
||||
log_step "Building clone image for $device_type..."
|
||||
local image_file
|
||||
image_file=$(build_clone_image "$device_type") || {
|
||||
log_error "Failed to build clone image"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Inject clone config
|
||||
local token=""
|
||||
[[ -f "$CLONE_DIR/.clone_token" ]] && token=$(cat "$CLONE_DIR/.clone_token")
|
||||
inject_clone_config "$image_file" "$master_ip" "$token"
|
||||
|
||||
# Copy image to TFTP
|
||||
if [[ -d "$TFTP_ROOT" ]]; then
|
||||
log_step "Copying image to TFTP root..."
|
||||
|
||||
# Decompress if gzipped
|
||||
local tftp_image="$TFTP_ROOT/secubox-clone.img"
|
||||
if [[ "$image_file" == *.gz ]]; then
|
||||
gunzip -c "$image_file" > "$tftp_image"
|
||||
else
|
||||
cp "$image_file" "$tftp_image"
|
||||
fi
|
||||
|
||||
log_info "TFTP image ready: $tftp_image"
|
||||
log_info "Size: $(du -h "$tftp_image" | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
# Generate U-Boot commands for target
|
||||
local host_ip
|
||||
host_ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
|
||||
[[ -z "$host_ip" ]] && host_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Clone Image Ready${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Image: $image_file"
|
||||
echo "TFTP: $TFTP_ROOT/secubox-clone.img"
|
||||
echo ""
|
||||
echo -e "${BOLD}To flash target in U-Boot:${NC}"
|
||||
echo " setenv serverip $host_ip"
|
||||
echo " setenv ipaddr 192.168.1.100"
|
||||
echo " dhcp"
|
||||
echo " tftpboot 0x20000000 secubox-clone.img"
|
||||
echo " mmc dev 0"
|
||||
echo ' mmc write 0x20000000 0 ${filesize}'
|
||||
echo " reset"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Save state
|
||||
cat > "$CLONE_DIR/.pull_state" <<EOF
|
||||
IMAGE_FILE="$image_file"
|
||||
TFTP_IMAGE="$TFTP_ROOT/secubox-clone.img"
|
||||
MASTER_IP="$master_ip"
|
||||
CLONE_TOKEN="$token"
|
||||
HOST_IP="$host_ip"
|
||||
DEVICE_TYPE="$device_type"
|
||||
PULL_TAG="$tag"
|
||||
EOF
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Flash Target
|
||||
# =============================================================================
|
||||
|
||||
cmd_flash() {
|
||||
local target="${TARGET_DEV:-}"
|
||||
local image=""
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target) target="$2"; shift 2 ;;
|
||||
--image) image="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
load_detected
|
||||
[[ -z "$target" ]] && target="$TARGET_DEV"
|
||||
[[ -z "$target" ]] && { log_error "No target device. Run: $0 detect"; return 1; }
|
||||
|
||||
# Load pull state
|
||||
if [[ -f "$CLONE_DIR/.pull_state" ]]; then
|
||||
source "$CLONE_DIR/.pull_state"
|
||||
[[ -z "$image" ]] && image="$TFTP_IMAGE"
|
||||
fi
|
||||
|
||||
[[ -z "$image" || ! -f "$image" ]] && {
|
||||
log_error "No clone image. Run: $0 pull first"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get host IP
|
||||
local host_ip="${HOST_IP:-}"
|
||||
[[ -z "$host_ip" ]] && host_ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
|
||||
[[ -z "$host_ip" ]] && host_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
local tag=$(get_tag)
|
||||
|
||||
echo -e "${BOLD}SecuBox Clone Station - Flash Target${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Target: $target"
|
||||
echo "Image: $image"
|
||||
echo "Host: $host_ip"
|
||||
echo ""
|
||||
|
||||
log_step "Entering U-Boot on target..."
|
||||
|
||||
# Use MOKATOOL to break into U-Boot and flash
|
||||
mokatool break --port "$target" --baud "$BAUDRATE" || {
|
||||
log_warn "Could not auto-break into U-Boot"
|
||||
log_info "Ensure target is at U-Boot prompt or reset it"
|
||||
}
|
||||
|
||||
log_step "Sending flash commands via MOKATOOL..."
|
||||
|
||||
# Create a temporary macro file for flashing
|
||||
local macro_file="$CLONE_DIR/flash-clone.yaml"
|
||||
cat > "$macro_file" <<MACRO
|
||||
macros:
|
||||
flash-clone:
|
||||
desc: "Flash SecuBox clone image via TFTP"
|
||||
steps:
|
||||
- send: "setenv serverip $host_ip"
|
||||
expect: "(=>|U-Boot>)"
|
||||
- send: "setenv ipaddr 192.168.1.100"
|
||||
expect: "(=>|U-Boot>)"
|
||||
- send: "dhcp"
|
||||
expect: "(=>|U-Boot>)"
|
||||
timeout: 30
|
||||
- send: "tftpboot 0x20000000 secubox-clone.img"
|
||||
expect: "(=>|U-Boot>)"
|
||||
timeout: 120
|
||||
- send: "mmc dev 0"
|
||||
expect: "(=>|U-Boot>)"
|
||||
- send: 'mmc write 0x20000000 0 \${filesize}'
|
||||
expect: "(=>|U-Boot>)"
|
||||
timeout: 300
|
||||
- send: "echo Flash complete, resetting..."
|
||||
expect: "(=>|U-Boot>)"
|
||||
- pause: 2
|
||||
- send: "reset"
|
||||
MACRO
|
||||
|
||||
mokatool macro --file "$macro_file" --name flash-clone --port "$target" \
|
||||
--baud "$BAUDRATE" --no-break-first 2>&1 | tee "$CLONE_LOGS/flash-$tag.log" || {
|
||||
log_warn "Flash may have failed or timed out"
|
||||
log_info "Check logs: $CLONE_LOGS/flash-$tag.log"
|
||||
}
|
||||
|
||||
echo ""
|
||||
log_info "Flash commands sent. Target should be rebooting..."
|
||||
log_info "Monitor progress: mokatool console --port $target"
|
||||
|
||||
# Save state
|
||||
cat > "$CLONE_DIR/.flash_state" <<EOF
|
||||
TARGET_DEV="$target"
|
||||
FLASH_TAG="$tag"
|
||||
FLASH_TIME="$(date -Iseconds)"
|
||||
EOF
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Verify Clone
|
||||
# =============================================================================
|
||||
|
||||
cmd_verify() {
|
||||
load_detected
|
||||
|
||||
local master="${MASTER_DEV:-}"
|
||||
local target="${TARGET_DEV:-}"
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--master) master="$2"; shift 2 ;;
|
||||
--target) target="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${BOLD}SecuBox Clone Station - Verify${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [[ -n "$target" && -c "$target" ]]; then
|
||||
log_step "Checking target status..."
|
||||
|
||||
# Wait for Linux boot
|
||||
log_info "Waiting for target to boot (up to 120s)..."
|
||||
local i=0
|
||||
while [ $i -lt 120 ]; do
|
||||
local response
|
||||
response=$(timeout 3 python3 -c "
|
||||
import serial
|
||||
import time
|
||||
try:
|
||||
ser = serial.Serial('$target', $BAUDRATE, timeout=1)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'\\n')
|
||||
time.sleep(0.5)
|
||||
print(ser.read(500).decode('utf-8', errors='ignore'))
|
||||
ser.close()
|
||||
except: pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$response" | grep -qE "(root@|login:)"; then
|
||||
log_info "Target booted!"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
i=$((i + 2))
|
||||
printf "\r Waiting... %ds " "$i"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Check mesh status on target
|
||||
log_step "Checking target mesh status..."
|
||||
python3 -c "
|
||||
import serial
|
||||
import time
|
||||
ser = serial.Serial('$target', $BAUDRATE, timeout=2)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'secubox master-link status 2>/dev/null || echo \"master-link not ready\"\\n')
|
||||
time.sleep(1)
|
||||
print(ser.read(1000).decode('utf-8', errors='ignore'))
|
||||
ser.close()
|
||||
" 2>/dev/null || echo "Could not check target"
|
||||
fi
|
||||
|
||||
if [[ -n "$master" && -c "$master" ]]; then
|
||||
log_step "Checking master for new peers..."
|
||||
|
||||
python3 -c "
|
||||
import serial
|
||||
import time
|
||||
ser = serial.Serial('$master', $BAUDRATE, timeout=2)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'secubox master-link peers 2>/dev/null || echo \"No peers command\"\\n')
|
||||
time.sleep(1)
|
||||
print(ser.read(2000).decode('utf-8', errors='ignore'))
|
||||
ser.close()
|
||||
" 2>/dev/null || echo "Could not check master"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Full Clone Workflow
|
||||
# =============================================================================
|
||||
|
||||
cmd_clone() {
|
||||
local master=""
|
||||
local target=""
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--master) master="$2"; shift 2 ;;
|
||||
--target) target="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
init_dirs
|
||||
|
||||
echo -e "${BOLD}SecuBox Clone Station - Full Workflow${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Step 1: Detect devices
|
||||
if [[ -z "$master" || -z "$target" ]]; then
|
||||
log_step "Step 1/4: Detecting devices..."
|
||||
detect_devices
|
||||
load_detected
|
||||
|
||||
[[ -z "$master" ]] && master="$MASTER_DEV"
|
||||
[[ -z "$target" ]] && target="$TARGET_DEV"
|
||||
else
|
||||
MASTER_DEV="$master"
|
||||
TARGET_DEV="$target"
|
||||
fi
|
||||
|
||||
[[ -z "$master" ]] && { log_error "No master device found"; return 1; }
|
||||
[[ -z "$target" ]] && { log_error "No target device found"; return 1; }
|
||||
|
||||
echo ""
|
||||
log_info "Master: $master"
|
||||
log_info "Target: $target"
|
||||
echo ""
|
||||
|
||||
# Step 2: Pull from master
|
||||
log_step "Step 2/4: Pulling image from master..."
|
||||
cmd_pull --master "$master"
|
||||
|
||||
echo ""
|
||||
read -p "Press Enter to flash target, or Ctrl+C to cancel..." _
|
||||
|
||||
# Step 3: Flash target
|
||||
log_step "Step 3/4: Flashing target..."
|
||||
cmd_flash --target "$target"
|
||||
|
||||
echo ""
|
||||
log_info "Waiting 60s for target to boot..."
|
||||
sleep 60
|
||||
|
||||
# Step 4: Verify
|
||||
log_step "Step 4/4: Verifying clone..."
|
||||
cmd_verify --master "$master" --target "$target"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Clone Complete!${NC}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Master: $master"
|
||||
echo "Target: $target (now a mesh peer)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " - Monitor target: mokatool console --port $target"
|
||||
echo " - Check mesh: ssh root@\$(target_ip) secubox master-link status"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MOKATOOL Helpers
|
||||
# =============================================================================
|
||||
|
||||
cmd_console() {
|
||||
local device="${1:-}"
|
||||
load_detected
|
||||
|
||||
[[ -z "$device" ]] && device="${MASTER_DEV:-${TARGET_DEV:-}}"
|
||||
[[ -z "$device" ]] && { log_error "No device specified"; return 1; }
|
||||
|
||||
log_info "Connecting to $device..."
|
||||
mokatool console --port "$device" --baud "$BAUDRATE"
|
||||
}
|
||||
|
||||
cmd_uboot() {
|
||||
local device="${1:-}"
|
||||
load_detected
|
||||
|
||||
[[ -z "$device" ]] && device="${TARGET_DEV:-}"
|
||||
[[ -z "$device" ]] && { log_error "No device specified"; return 1; }
|
||||
|
||||
log_info "Breaking into U-Boot on $device..."
|
||||
mokatool break --port "$device" --baud "$BAUDRATE"
|
||||
}
|
||||
|
||||
cmd_env_backup() {
|
||||
local device="${1:-}"
|
||||
local output="${2:-$CLONE_DIR/uboot-env-$(get_tag).txt}"
|
||||
|
||||
load_detected
|
||||
[[ -z "$device" ]] && device="${TARGET_DEV:-${MASTER_DEV:-}}"
|
||||
[[ -z "$device" ]] && { log_error "No device specified"; return 1; }
|
||||
|
||||
log_step "Backing up U-Boot environment from $device..."
|
||||
|
||||
# Break into U-Boot and dump env
|
||||
mokatool break --port "$device" --baud "$BAUDRATE" 2>/dev/null || true
|
||||
|
||||
python3 -c "
|
||||
import serial
|
||||
import time
|
||||
ser = serial.Serial('$device', $BAUDRATE, timeout=3)
|
||||
time.sleep(0.2)
|
||||
ser.write(b'printenv\\n')
|
||||
time.sleep(2)
|
||||
output = ser.read(10000).decode('utf-8', errors='ignore')
|
||||
# Filter to actual env vars
|
||||
for line in output.split('\\n'):
|
||||
if '=' in line and not line.startswith(' ') and not line.startswith('printenv'):
|
||||
print(line.strip())
|
||||
ser.close()
|
||||
" > "$output" 2>/dev/null
|
||||
|
||||
log_info "U-Boot environment saved: $output"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Usage
|
||||
# =============================================================================
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
SecuBox Clone Station - Host-side Device Cloner
|
||||
|
||||
Usage: secubox-clone-station.sh <command> [options]
|
||||
|
||||
Commands:
|
||||
detect Detect USB serial devices (master/target)
|
||||
pull [--master DEV] Pull clone image from master device
|
||||
flash [--target DEV] Flash clone image to target via U-Boot
|
||||
verify Verify clone joined mesh
|
||||
clone Full workflow: detect → pull → flash → verify
|
||||
|
||||
console [DEV] Connect to serial console (via MOKATOOL)
|
||||
uboot [DEV] Break into U-Boot prompt
|
||||
env-backup [DEV] [FILE] Backup U-Boot environment
|
||||
|
||||
Options:
|
||||
--master DEV Master device (e.g., /dev/ttyUSB0)
|
||||
--target DEV Target device (e.g., /dev/ttyUSB1)
|
||||
--image FILE Clone image file (for flash)
|
||||
|
||||
Environment:
|
||||
MOKATOOL_DIR Path to MOKATOOL (default: ~/DEVEL/MOKATOOL)
|
||||
TFTP_ROOT TFTP root directory (default: /srv/tftp)
|
||||
|
||||
Examples:
|
||||
# Auto-detect devices
|
||||
./secubox-clone-station.sh detect
|
||||
|
||||
# Full clone workflow
|
||||
./secubox-clone-station.sh clone
|
||||
|
||||
# Manual workflow
|
||||
./secubox-clone-station.sh pull --master /dev/ttyUSB0
|
||||
./secubox-clone-station.sh flash --target /dev/ttyUSB1
|
||||
|
||||
# Interactive console
|
||||
./secubox-clone-station.sh console /dev/ttyUSB0
|
||||
|
||||
Requirements:
|
||||
- MOKATOOL (mochabin_tool.py) with pyserial, pexpect, rich, typer
|
||||
- TFTP server configured and running
|
||||
- Both devices connected via USB serial
|
||||
EOF
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
check_deps || exit 1
|
||||
init_dirs
|
||||
|
||||
case "${1:-}" in
|
||||
detect)
|
||||
detect_devices
|
||||
;;
|
||||
pull)
|
||||
shift
|
||||
cmd_pull "$@"
|
||||
;;
|
||||
flash)
|
||||
shift
|
||||
cmd_flash "$@"
|
||||
;;
|
||||
verify)
|
||||
shift
|
||||
cmd_verify "$@"
|
||||
;;
|
||||
clone)
|
||||
shift
|
||||
cmd_clone "$@"
|
||||
;;
|
||||
console)
|
||||
shift
|
||||
cmd_console "$@"
|
||||
;;
|
||||
uboot)
|
||||
shift
|
||||
cmd_uboot "$@"
|
||||
;;
|
||||
env-backup|env-dump)
|
||||
shift
|
||||
cmd_env_backup "$@"
|
||||
;;
|
||||
help|--help|-h|"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $1"
|
||||
echo ""
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user