feat(cloner): Add multi-device image support

- Support building images for: mochabin, espressobin-v7, espressobin-ultra, x86-64
- New CLI: secubox-cloner build --device espressobin-v7
- New CLI: secubox-cloner devices (list supported devices)
- RPCD: list_devices method, build_image accepts device_type param
- LuCI: Device selection dropdown in build modal
- LuCI: Device column in images table with badges
- Each device type has its own TFTP image file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-11 06:17:12 +01:00
parent 06d9d08f86
commit 750f79db3c
5 changed files with 172 additions and 49 deletions

View File

@ -326,7 +326,8 @@
"Bash(fuser:*)",
"Bash(lsof:*)",
"Bash(arp:*)",
"Bash(ip link:*)"
"Bash(ip link:*)",
"Bash(git describe:*)"
]
}
}

View File

@ -37,7 +37,14 @@ var callGenerateToken = rpc.declare({
var callBuildImage = rpc.declare({
object: 'luci.cloner',
method: 'build_image'
method: 'build_image',
params: ['device_type']
});
var callListDevices = rpc.declare({
object: 'luci.cloner',
method: 'list_devices',
expect: { devices: [] }
});
var callTftpStart = rpc.declare({
@ -62,7 +69,8 @@ return view.extend({
callGetStatus(),
callListImages(),
callListTokens(),
callListClones()
callListClones(),
callListDevices()
]);
},
@ -71,6 +79,7 @@ return view.extend({
var images = data[1].images || [];
var tokens = data[2].tokens || [];
var clones = data[3].clones || [];
var devices = data[4].devices || [];
var view = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, 'Cloning Station'),
@ -117,13 +126,14 @@ return view.extend({
E('h3', {}, 'Clone Images'),
E('table', { 'class': 'table', 'id': 'images-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, 'Device'),
E('th', { 'class': 'th' }, 'Name'),
E('th', { 'class': 'th' }, 'Size'),
E('th', { 'class': 'th' }, 'TFTP Ready'),
E('th', { 'class': 'th' }, 'Actions')
])
].concat(images.length > 0 ? images.map(L.bind(this.renderImageRow, this)) :
[E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 4, 'style': 'text-align:center;' },
[E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 5, 'style': 'text-align:center;' },
'No images available. Click "Build Image" to create one.')])]
))
]),
@ -194,10 +204,17 @@ return view.extend({
},
renderImageRow: function(img) {
var deviceBadge = E('span', {
'style': 'padding:2px 8px;border-radius:4px;font-size:12px;background:#3b82f622;color:#3b82f6;'
}, img.device || 'unknown');
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, img.name),
E('td', { 'class': 'td' }, deviceBadge),
E('td', { 'class': 'td', 'style': 'font-family:monospace;font-size:12px;' }, img.name),
E('td', { 'class': 'td' }, img.size),
E('td', { 'class': 'td' }, img.tftp_ready ? 'Yes' : 'No'),
E('td', { 'class': 'td' }, img.tftp_ready ?
E('span', { 'style': 'color:#22c55e;' }, 'Ready') :
E('span', { 'style': 'color:#f59e0b;' }, 'Pending')),
E('td', { 'class': 'td' }, '-')
]);
},
@ -231,20 +248,34 @@ return view.extend({
},
handleBuild: function() {
ui.showModal('Build Clone Image', [
E('p', {}, 'This will build a clone image of the current system.'),
E('p', {}, 'The image can then be flashed to other devices of the same type.'),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'),
' ',
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': L.bind(function() {
ui.hideModal();
callBuildImage().then(L.bind(function(res) {
ui.addNotification(null, E('p', res.message || 'Build started'), 'info');
}, this));
}, this) }, 'Build')
])
]);
var self = this;
callListDevices().then(function(data) {
var devices = data.devices || [];
var select = E('select', { 'id': 'device-select', 'class': 'cbi-input-select', 'style': 'width:100%;' });
devices.forEach(function(dev) {
select.appendChild(E('option', { 'value': dev.id }, dev.name + ' (' + dev.cpu + ')'));
});
ui.showModal('Build Clone Image', [
E('p', {}, 'Select the target device type to build an image for:'),
E('div', { 'style': 'margin:15px 0;' }, select),
E('p', { 'style': 'color:#888;font-size:12px;' }, 'The image will be built via ASU API and may take several minutes.'),
E('div', { 'class': 'right' }, [
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'),
' ',
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
var deviceType = document.getElementById('device-select').value;
ui.hideModal();
ui.addNotification(null, E('p', 'Building image for ' + deviceType + '...'), 'info');
callBuildImage(deviceType).then(function(res) {
ui.addNotification(null, E('p', res.message || 'Build started'), 'info');
self.refresh();
});
} }, 'Build')
])
]);
});
},
handleTftp: function(start) {

View File

@ -86,30 +86,41 @@ do_status() {
}
do_list_images() {
local img name
local img name device_type
json_init
json_add_array "images"
# TFTP ready image
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
# All TFTP-ready images
for img in "$TFTP_ROOT"/secubox-clone*.img; do
[ -f "$img" ] || continue
name=$(basename "$img")
# Extract device type from filename
device_type=$(echo "$name" | sed -n 's/secubox-clone-\(.*\)\.img/\1/p')
[ -z "$device_type" ] && device_type="mochabin"
json_add_object ""
json_add_string "name" "secubox-clone.img"
json_add_string "path" "$TFTP_ROOT/secubox-clone.img"
json_add_string "size" "$(ls -lh "$TFTP_ROOT/secubox-clone.img" | awk '{print $5}')"
json_add_string "name" "$name"
json_add_string "path" "$img"
json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')"
json_add_string "device" "$device_type"
json_add_boolean "tftp_ready" 1
json_close_object
fi
done
# Clone directory images
# Clone directory images (not yet in TFTP)
if [ -d "$CLONE_DIR" ]; then
for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz; do
[ -f "$img" ] || continue
name=$(basename "$img")
# Skip if already in TFTP
[ -f "$TFTP_ROOT/${name%.gz}" ] && continue
device_type=$(echo "$name" | sed -n 's/secubox-clone-\(.*\)\.img.*/\1/p')
[ -z "$device_type" ] && device_type="unknown"
json_add_object ""
json_add_string "name" "$name"
json_add_string "path" "$img"
json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')"
json_add_string "device" "$device_type"
json_add_boolean "tftp_ready" 0
json_close_object
done
@ -219,12 +230,23 @@ EOF
}
do_build_image() {
local input device_type
read input
device_type=$(echo "$input" | jsonfilter -e '@.device_type' 2>/dev/null)
json_init
if [ -x /usr/sbin/secubox-cloner ]; then
(/usr/sbin/secubox-cloner build 2>&1 > /tmp/cloner-build.log) &
json_add_boolean "success" 1
json_add_string "message" "Build started in background"
if [ -n "$device_type" ]; then
(/usr/sbin/secubox-cloner build "$device_type" 2>&1 > /tmp/cloner-build.log) &
json_add_boolean "success" 1
json_add_string "message" "Build started for $device_type"
else
(/usr/sbin/secubox-cloner build 2>&1 > /tmp/cloner-build.log) &
json_add_boolean "success" 1
json_add_string "message" "Build started for current device"
fi
else
json_add_boolean "success" 0
json_add_string "message" "secubox-cloner not installed"
@ -233,6 +255,38 @@ do_build_image() {
json_dump
}
do_list_devices() {
json_init
json_add_array "devices"
json_add_object ""
json_add_string "id" "mochabin"
json_add_string "name" "Globalscale MOCHAbin"
json_add_string "cpu" "Cortex-A72"
json_close_object
json_add_object ""
json_add_string "id" "espressobin-v7"
json_add_string "name" "Globalscale ESPRESSObin v7"
json_add_string "cpu" "Cortex-A53"
json_close_object
json_add_object ""
json_add_string "id" "espressobin-ultra"
json_add_string "name" "Globalscale ESPRESSObin Ultra"
json_add_string "cpu" "Cortex-A53"
json_close_object
json_add_object ""
json_add_string "id" "x86-64"
json_add_string "name" "Generic x86-64"
json_add_string "cpu" "x86_64"
json_close_object
json_close_array
json_dump
}
do_tftp_start() {
json_init
@ -301,8 +355,9 @@ case "$1" in
echo '"list_images":{},'
echo '"list_tokens":{},'
echo '"list_clones":{},'
echo '"list_devices":{},'
echo '"generate_token":{"auto_approve":"Boolean"},'
echo '"build_image":{},'
echo '"build_image":{"device_type":"String"},'
echo '"tftp_start":{},'
echo '"tftp_stop":{},'
echo '"delete_token":{"token":"String"},'
@ -315,6 +370,7 @@ case "$1" in
list_images) do_list_images ;;
list_tokens) do_list_tokens ;;
list_clones) do_list_clones ;;
list_devices) do_list_devices ;;
generate_token) do_generate_token ;;
build_image) do_build_image ;;
tftp_start) do_tftp_start ;;

View File

@ -3,7 +3,7 @@
"description": "Grant access to SecuBox Cloning Station",
"read": {
"ubus": {
"luci.cloner": ["status", "list_images", "list_tokens", "list_clones"]
"luci.cloner": ["status", "list_images", "list_tokens", "list_clones", "list_devices"]
}
},
"write": {

View File

@ -6,8 +6,9 @@
# Cloned devices auto-resize root and join mesh as peers.
#
# Usage:
# secubox-cloner build [--resize SIZE] Build clone image for current device
# secubox-cloner build [--device TYPE] [--resize SIZE] Build clone image
# secubox-cloner serve [--start|--stop] Manage TFTP clone server
# secubox-cloner devices List supported device types
# secubox-cloner token [--auto-approve] Generate clone join token
# secubox-cloner status Show cloner status
# secubox-cloner list List pending/joined clones
@ -96,31 +97,41 @@ get_device_id() {
# ============================================================================
build_image() {
local resize_target="${1:-}"
local device_type="${1:-}"
local resize_target="${2:-}"
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
# Use specified device type or detect current
if [ -z "$device_type" ]; then
device_type=$(detect_device)
fi
# Validate device type
case "$device_type" in
mochabin|espressobin-v7|espressobin-ultra|x86-64)
;;
*)
error "Unknown device type: $device_type"
error "Supported: mochabin, espressobin-v7, espressobin-ultra, x86-64"
return 1
;;
esac
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
# Image paths - each device type has its own image
local image_name="secubox-clone-${device_type}.img"
local image_path="$CLONE_DIR/$image_name"
local tftp_image="$TFTP_ROOT/${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
# Check if we have a pre-built image for this device type
local existing_image=$(ls -t "$CLONE_DIR"/secubox-clone-${device_type}.img* 2>/dev/null | head -1)
if [ -n "$existing_image" ] && [ -f "$existing_image" ]; then
log "Using existing image: $existing_image"
image_path="$existing_image"
else
@ -144,10 +155,9 @@ build_image() {
resize_image "$image_path" "$resize_target"
fi
# Copy to TFTP root
# Copy to TFTP root with device-specific name
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
@ -155,18 +165,36 @@ build_image() {
fi
log "Clone image ready: $tftp_image"
log "Size: $(ls -lh "$tftp_image" | awk '{print $5}')"
# Also create generic symlink for backward compatibility
ln -sf "$image_name" "$TFTP_ROOT/secubox-clone.img" 2>/dev/null
fi
# Save state
cat > "$STATE_FILE" <<EOF
DEVICE_TYPE="$device_type"
IMAGE_PATH="$image_path"
TFTP_IMAGE="$TFTP_ROOT/secubox-clone.img"
TFTP_IMAGE="$tftp_image"
BUILD_TIME="$(date -Iseconds)"
LAN_IP="$(get_lan_ip)"
EOF
return 0
}
# List supported devices
list_devices() {
echo ""
echo -e "${BOLD}Supported Device Types${NC}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " mochabin Globalscale MOCHAbin (Cortex-A72)"
echo " espressobin-v7 Globalscale ESPRESSObin v7 (Cortex-A53)"
echo " espressobin-ultra Globalscale ESPRESSObin Ultra"
echo " x86-64 Generic x86-64 PC"
echo ""
echo "Current device: $(detect_device)"
echo ""
}
build_via_asu() {
@ -863,14 +891,21 @@ EOF
case "${1:-}" in
build)
shift
device_type=""
resize=""
while [ $# -gt 0 ]; do
case "$1" in
--device|-d) device_type="$2"; shift 2 ;;
--resize) resize="$2"; shift 2 ;;
mochabin|espressobin-v7|espressobin-ultra|x86-64) device_type="$1"; shift ;;
*) shift ;;
esac
done
build_image "$resize"
build_image "$device_type" "$resize"
;;
devices)
list_devices
;;
serve)