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:
parent
06d9d08f86
commit
750f79db3c
@ -326,7 +326,8 @@
|
||||
"Bash(fuser:*)",
|
||||
"Bash(lsof:*)",
|
||||
"Bash(arp:*)",
|
||||
"Bash(ip link:*)"
|
||||
"Bash(ip link:*)",
|
||||
"Bash(git describe:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 ;;
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user