feat(luci-app-cloner): Add cloning station LuCI dashboard
- Dashboard with status cards: device, TFTP, tokens, clones - Quick actions: Build Image, Start/Stop TFTP, Token generation - Clone images table with size and TFTP-ready status - Token management with auto-approve option - U-Boot flash commands display when TFTP is running - RPCD handler with 10 methods for full cloner management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0e5965dd6c
commit
06d9d08f86
@ -1040,3 +1040,13 @@ _Last updated: 2026-02-10_
|
|||||||
- Live indicator with timestamp display.
|
- Live indicator with timestamp display.
|
||||||
- Efficient updateValue/updateBar/updateList methods.
|
- Efficient updateValue/updateBar/updateList methods.
|
||||||
- Tag: v0.19.16
|
- Tag: v0.19.16
|
||||||
|
|
||||||
|
34. **Cloning Station LuCI Dashboard (2026-02-11)**
|
||||||
|
- Created `luci-app-cloner` package for station cloning management.
|
||||||
|
- Dashboard shows: device type, TFTP status, image info, tokens, clones.
|
||||||
|
- Quick actions: Build Image, Start/Stop TFTP, New Token, Auto-Approve Token.
|
||||||
|
- Clone images table with TFTP-ready status.
|
||||||
|
- Token management with create/delete functionality.
|
||||||
|
- U-Boot flash commands display when TFTP is running.
|
||||||
|
- RPCD handler with 10 methods for status, images, tokens, clones.
|
||||||
|
- Tag: v0.19.20
|
||||||
|
|||||||
@ -120,6 +120,15 @@ _Last updated: 2026-02-09 (early morning)_
|
|||||||
- Added `secubox clone` and `secubox master-link` CLI command groups
|
- Added `secubox clone` and `secubox master-link` CLI command groups
|
||||||
- Full workflow: build image on master → TFTP serve → flash target → auto-join mesh
|
- Full workflow: build image on master → TFTP serve → flash target → auto-join mesh
|
||||||
|
|
||||||
|
- **Cloning Station LuCI Dashboard** — DONE (2026-02-11)
|
||||||
|
- Created `luci-app-cloner` package with KISS-style dashboard
|
||||||
|
- Status cards: device type, TFTP status, token count, clone count
|
||||||
|
- Quick actions: Build Image, Start/Stop TFTP, New/Auto-Approve Token
|
||||||
|
- Clone images table with size and TFTP-ready indicator
|
||||||
|
- Token management with delete functionality
|
||||||
|
- U-Boot flash commands display when TFTP active
|
||||||
|
- RPCD handler: 10 methods (status, list_images, list_tokens, list_clones, etc.)
|
||||||
|
|
||||||
- **HAProxy "End of Internet" Default Page** — DONE (2026-02-07)
|
- **HAProxy "End of Internet" Default Page** — DONE (2026-02-07)
|
||||||
- Cyberpunk fallback page for unknown/unmatched domains
|
- Cyberpunk fallback page for unknown/unmatched domains
|
||||||
- Matrix rain animation, glitch text, ASCII art SecuBox logo
|
- Matrix rain animation, glitch text, ASCII art SecuBox logo
|
||||||
|
|||||||
32
package/secubox/luci-app-cloner/Makefile
Normal file
32
package/secubox/luci-app-cloner/Makefile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI Cloning Station Dashboard
|
||||||
|
LUCI_DESCRIPTION:=SecuBox cloning station for building and deploying clone images
|
||||||
|
LUCI_DEPENDS:=+luci-base +rpcd +secubox-core
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-cloner
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_MAINTAINER:=SecuBox Team
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/conffiles
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-cloner.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-cloner.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.cloner $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/cloner
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/cloner/*.js $(1)/www/luci-static/resources/view/cloner/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||||
@ -0,0 +1,331 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require poll';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
var callGetStatus = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'status',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListImages = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'list_images',
|
||||||
|
expect: { images: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListTokens = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'list_tokens',
|
||||||
|
expect: { tokens: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListClones = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'list_clones',
|
||||||
|
expect: { clones: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGenerateToken = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'generate_token',
|
||||||
|
params: ['auto_approve']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callBuildImage = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'build_image'
|
||||||
|
});
|
||||||
|
|
||||||
|
var callTftpStart = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'tftp_start'
|
||||||
|
});
|
||||||
|
|
||||||
|
var callTftpStop = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'tftp_stop'
|
||||||
|
});
|
||||||
|
|
||||||
|
var callDeleteToken = rpc.declare({
|
||||||
|
object: 'luci.cloner',
|
||||||
|
method: 'delete_token',
|
||||||
|
params: ['token']
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
callGetStatus(),
|
||||||
|
callListImages(),
|
||||||
|
callListTokens(),
|
||||||
|
callListClones()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0] || {};
|
||||||
|
var images = data[1].images || [];
|
||||||
|
var tokens = data[2].tokens || [];
|
||||||
|
var clones = data[3].clones || [];
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', {}, 'Cloning Station'),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, 'Build and deploy SecuBox clone images to new devices'),
|
||||||
|
|
||||||
|
// Status Cards
|
||||||
|
E('div', { 'style': 'display:flex;gap:20px;margin:20px 0;flex-wrap:wrap;' }, [
|
||||||
|
E('div', { 'style': 'padding:15px;background:#3b82f622;border-radius:8px;min-width:120px;' }, [
|
||||||
|
E('div', { 'style': 'font-size:12px;color:#888;' }, 'Device'),
|
||||||
|
E('strong', { 'style': 'font-size:16px;color:#3b82f6;' }, status.device_type || 'unknown')
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'padding:15px;border-radius:8px;min-width:120px;', 'class': status.tftp_running ? 'tftp-on' : 'tftp-off' }, [
|
||||||
|
E('div', { 'style': 'font-size:12px;color:#888;' }, 'TFTP'),
|
||||||
|
E('strong', { 'style': 'font-size:16px;', 'id': 'tftp-status' },
|
||||||
|
status.tftp_running ? 'Running' : 'Stopped')
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'padding:15px;background:#8b5cf622;border-radius:8px;min-width:120px;' }, [
|
||||||
|
E('div', { 'style': 'font-size:12px;color:#888;' }, 'Tokens'),
|
||||||
|
E('strong', { 'style': 'font-size:24px;color:#8b5cf6;', 'id': 'token-count' },
|
||||||
|
String(tokens.length))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'padding:15px;background:#22c55e22;border-radius:8px;min-width:120px;' }, [
|
||||||
|
E('div', { 'style': 'font-size:12px;color:#888;' }, 'Clones'),
|
||||||
|
E('strong', { 'style': 'font-size:24px;color:#22c55e;', 'id': 'clone-count' },
|
||||||
|
String(status.clone_count || 0))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Quick Actions
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Quick Actions'),
|
||||||
|
E('div', { 'style': 'display:flex;gap:10px;flex-wrap:wrap;' }, [
|
||||||
|
this.createActionButton('Build Image', 'cbi-button-action', L.bind(this.handleBuild, this)),
|
||||||
|
this.createActionButton(status.tftp_running ? 'Stop TFTP' : 'Start TFTP',
|
||||||
|
status.tftp_running ? 'cbi-button-negative' : 'cbi-button-positive',
|
||||||
|
L.bind(this.handleTftp, this, !status.tftp_running)),
|
||||||
|
this.createActionButton('New Token', 'cbi-button-action', L.bind(this.handleNewToken, this)),
|
||||||
|
this.createActionButton('Auto-Approve Token', 'cbi-button-save', L.bind(this.handleAutoToken, this))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Clone Images
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Clone Images'),
|
||||||
|
E('table', { 'class': 'table', 'id': 'images-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
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;' },
|
||||||
|
'No images available. Click "Build Image" to create one.')])]
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Tokens
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Clone Tokens'),
|
||||||
|
E('table', { 'class': 'table', 'id': 'tokens-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'Token'),
|
||||||
|
E('th', { 'class': 'th' }, 'Created'),
|
||||||
|
E('th', { 'class': 'th' }, 'Type'),
|
||||||
|
E('th', { 'class': 'th' }, 'Actions')
|
||||||
|
])
|
||||||
|
].concat(tokens.length > 0 ? tokens.map(L.bind(this.renderTokenRow, this)) :
|
||||||
|
[E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 4, 'style': 'text-align:center;' },
|
||||||
|
'No tokens. Click "New Token" to generate one.')])]
|
||||||
|
))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// TFTP Instructions
|
||||||
|
status.tftp_running ? E('div', { 'class': 'cbi-section', 'style': 'background:#22c55e11;padding:15px;border-radius:8px;border-left:4px solid #22c55e;' }, [
|
||||||
|
E('h3', { 'style': 'margin-top:0;' }, 'U-Boot Flash Commands'),
|
||||||
|
E('p', {}, 'Run these commands in U-Boot (Marvell>> prompt) on the target device:'),
|
||||||
|
E('pre', { 'style': 'background:#000;color:#0f0;padding:10px;border-radius:4px;overflow-x:auto;' },
|
||||||
|
'setenv serverip ' + status.lan_ip + '\n' +
|
||||||
|
'setenv ipaddr 192.168.255.100\n' +
|
||||||
|
'dhcp\n' +
|
||||||
|
'tftpboot 0x6000000 secubox-clone.img\n' +
|
||||||
|
'mmc dev 1\n' +
|
||||||
|
'mmc write 0x6000000 0 ${filesize}\n' +
|
||||||
|
'reset'
|
||||||
|
)
|
||||||
|
]) : null,
|
||||||
|
|
||||||
|
// Cloned Devices
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, 'Cloned Devices'),
|
||||||
|
E('table', { 'class': 'table', 'id': 'clones-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'Device'),
|
||||||
|
E('th', { 'class': 'th' }, 'Status')
|
||||||
|
])
|
||||||
|
].concat(clones.length > 0 ? clones.map(L.bind(this.renderCloneRow, this)) :
|
||||||
|
[E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 2, 'style': 'text-align:center;' },
|
||||||
|
'No clones yet.')])]
|
||||||
|
))
|
||||||
|
])
|
||||||
|
].filter(Boolean));
|
||||||
|
|
||||||
|
// Add dynamic styles
|
||||||
|
var style = E('style', {}, [
|
||||||
|
'.tftp-on { background: #22c55e22; }',
|
||||||
|
'.tftp-on strong { color: #22c55e; }',
|
||||||
|
'.tftp-off { background: #64748b22; }',
|
||||||
|
'.tftp-off strong { color: #64748b; }'
|
||||||
|
].join('\n'));
|
||||||
|
view.insertBefore(style, view.firstChild);
|
||||||
|
|
||||||
|
poll.add(L.bind(this.refresh, this), 10);
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
createActionButton: function(label, cls, handler) {
|
||||||
|
var btn = E('button', { 'class': 'cbi-button ' + cls, 'style': 'padding:8px 16px;' }, label);
|
||||||
|
btn.addEventListener('click', handler);
|
||||||
|
return btn;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderImageRow: function(img) {
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, img.name),
|
||||||
|
E('td', { 'class': 'td' }, img.size),
|
||||||
|
E('td', { 'class': 'td' }, img.tftp_ready ? 'Yes' : 'No'),
|
||||||
|
E('td', { 'class': 'td' }, '-')
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTokenRow: function(tok) {
|
||||||
|
var typeLabel = tok.auto_approve ? 'Auto-Approve' : 'Manual';
|
||||||
|
var usedLabel = tok.used ? ' (used)' : '';
|
||||||
|
var style = tok.used ? 'opacity:0.5;' : '';
|
||||||
|
|
||||||
|
var deleteBtn = E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-negative',
|
||||||
|
'style': 'padding:2px 8px;font-size:12px;',
|
||||||
|
'data-token': tok.token
|
||||||
|
}, 'Delete');
|
||||||
|
deleteBtn.addEventListener('click', L.bind(this.handleDeleteToken, this));
|
||||||
|
|
||||||
|
return E('tr', { 'class': 'tr', 'style': style }, [
|
||||||
|
E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, tok.token_short),
|
||||||
|
E('td', { 'class': 'td' }, tok.created ? tok.created.split('T')[0] : '-'),
|
||||||
|
E('td', { 'class': 'td' }, typeLabel + usedLabel),
|
||||||
|
E('td', { 'class': 'td' }, deleteBtn)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCloneRow: function(clone) {
|
||||||
|
var statusColor = clone.status === 'active' ? '#22c55e' : '#f59e0b';
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, clone.info || '-'),
|
||||||
|
E('td', { 'class': 'td' }, E('span', { 'style': 'color:' + statusColor }, clone.status))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
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')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTftp: function(start) {
|
||||||
|
var fn = start ? callTftpStart : callTftpStop;
|
||||||
|
fn().then(L.bind(function(res) {
|
||||||
|
ui.addNotification(null, E('p', res.message || (start ? 'TFTP started' : 'TFTP stopped')), 'info');
|
||||||
|
this.refresh();
|
||||||
|
}, this));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleNewToken: function() {
|
||||||
|
callGenerateToken(false).then(L.bind(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.showModal('Token Generated', [
|
||||||
|
E('p', {}, 'New clone token created:'),
|
||||||
|
E('pre', { 'style': 'background:#f1f5f9;padding:10px;border-radius:4px;word-break:break-all;' }, res.token),
|
||||||
|
E('p', { 'style': 'color:#888;' }, 'This token requires manual approval when used.'),
|
||||||
|
E('div', { 'class': 'right' }, [
|
||||||
|
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
|
||||||
|
ui.hideModal();
|
||||||
|
} }, 'OK')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAutoToken: function() {
|
||||||
|
callGenerateToken(true).then(L.bind(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.showModal('Auto-Approve Token Generated', [
|
||||||
|
E('p', {}, 'New auto-approve token created:'),
|
||||||
|
E('pre', { 'style': 'background:#22c55e22;padding:10px;border-radius:4px;word-break:break-all;' }, res.token),
|
||||||
|
E('p', { 'style': 'color:#22c55e;' }, 'Devices using this token will auto-join the mesh without manual approval.'),
|
||||||
|
E('div', { 'class': 'right' }, [
|
||||||
|
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
|
||||||
|
ui.hideModal();
|
||||||
|
} }, 'OK')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}, this));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDeleteToken: function(ev) {
|
||||||
|
var token = ev.currentTarget.dataset.token;
|
||||||
|
if (confirm('Delete this token?')) {
|
||||||
|
callDeleteToken(token).then(L.bind(function() {
|
||||||
|
this.refresh();
|
||||||
|
}, this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function() {
|
||||||
|
return Promise.all([
|
||||||
|
callGetStatus(),
|
||||||
|
callListImages(),
|
||||||
|
callListTokens(),
|
||||||
|
callListClones()
|
||||||
|
]).then(L.bind(function(data) {
|
||||||
|
var status = data[0] || {};
|
||||||
|
var tokens = data[2].tokens || [];
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
var tftpEl = document.getElementById('tftp-status');
|
||||||
|
var tokenEl = document.getElementById('token-count');
|
||||||
|
var cloneEl = document.getElementById('clone-count');
|
||||||
|
|
||||||
|
if (tftpEl) {
|
||||||
|
tftpEl.textContent = status.tftp_running ? 'Running' : 'Stopped';
|
||||||
|
tftpEl.parentNode.parentNode.className = status.tftp_running ? 'tftp-on' : 'tftp-off';
|
||||||
|
}
|
||||||
|
if (tokenEl) tokenEl.textContent = String(tokens.length);
|
||||||
|
if (cloneEl) cloneEl.textContent = String(status.clone_count || 0);
|
||||||
|
|
||||||
|
}, this));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
326
package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner
Executable file
326
package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner
Executable file
@ -0,0 +1,326 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# RPCD handler for SecuBox Cloner
|
||||||
|
#
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
CLONE_DIR="/srv/secubox/clone"
|
||||||
|
TFTP_ROOT="/srv/tftp"
|
||||||
|
TOKENS_DIR="/var/run/secubox/clone-tokens"
|
||||||
|
STATE_FILE="/var/run/secubox/cloner.state"
|
||||||
|
|
||||||
|
# Detect device type
|
||||||
|
detect_device() {
|
||||||
|
local board_name=""
|
||||||
|
[ -f /tmp/sysinfo/board_name ] && board_name=$(cat /tmp/sysinfo/board_name)
|
||||||
|
[ -z "$board_name" ] && board_name=$(uci -q get system.@system[0].hostname 2>/dev/null)
|
||||||
|
|
||||||
|
case "$board_name" in
|
||||||
|
*mochabin*|*MOCHAbin*|globalscale,mochabin) echo "mochabin" ;;
|
||||||
|
*espressobin*ultra*) echo "espressobin-ultra" ;;
|
||||||
|
*espressobin*) echo "espressobin-v7" ;;
|
||||||
|
*x86*|*generic*) echo "x86-64" ;;
|
||||||
|
*) echo "unknown" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_lan_ip() {
|
||||||
|
uci -q get network.lan.ipaddr 2>/dev/null || echo "192.168.255.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_status() {
|
||||||
|
local device_type lan_ip hostname tftp_enabled
|
||||||
|
local has_image image_size image_name token_count clone_count
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
# Device info
|
||||||
|
device_type=$(detect_device)
|
||||||
|
lan_ip=$(get_lan_ip)
|
||||||
|
hostname=$(uci -q get system.@system[0].hostname || echo "secubox")
|
||||||
|
|
||||||
|
json_add_string "device_type" "$device_type"
|
||||||
|
json_add_string "lan_ip" "$lan_ip"
|
||||||
|
json_add_string "hostname" "$hostname"
|
||||||
|
|
||||||
|
# TFTP status
|
||||||
|
tftp_enabled=$(uci -q get dhcp.@dnsmasq[0].enable_tftp)
|
||||||
|
json_add_boolean "tftp_running" "$([ "$tftp_enabled" = "1" ] && echo 1 || echo 0)"
|
||||||
|
json_add_string "tftp_root" "$TFTP_ROOT"
|
||||||
|
|
||||||
|
# Image status
|
||||||
|
has_image=0
|
||||||
|
image_size=""
|
||||||
|
image_name=""
|
||||||
|
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
|
||||||
|
has_image=1
|
||||||
|
image_size=$(ls -lh "$TFTP_ROOT/secubox-clone.img" 2>/dev/null | awk '{print $5}')
|
||||||
|
image_name="secubox-clone.img"
|
||||||
|
fi
|
||||||
|
json_add_boolean "has_image" "$has_image"
|
||||||
|
json_add_string "image_size" "${image_size:-0}"
|
||||||
|
json_add_string "image_name" "${image_name:-}"
|
||||||
|
|
||||||
|
# Token count
|
||||||
|
token_count=0
|
||||||
|
[ -d "$TOKENS_DIR" ] && token_count=$(ls "$TOKENS_DIR"/*.json 2>/dev/null | wc -l)
|
||||||
|
json_add_int "token_count" "$token_count"
|
||||||
|
|
||||||
|
# Clone count (from master-link peer-list)
|
||||||
|
clone_count=0
|
||||||
|
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||||
|
clone_count=$(/usr/lib/secubox/master-link.sh peer-list 2>/dev/null | grep -c "^[0-9]" || echo 0)
|
||||||
|
fi
|
||||||
|
json_add_int "clone_count" "$clone_count"
|
||||||
|
|
||||||
|
# Build state
|
||||||
|
if [ -f "$STATE_FILE" ]; then
|
||||||
|
. "$STATE_FILE"
|
||||||
|
json_add_string "last_build" "${BUILD_TIME:-}"
|
||||||
|
else
|
||||||
|
json_add_string "last_build" ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_list_images() {
|
||||||
|
local img name
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_array "images"
|
||||||
|
|
||||||
|
# TFTP ready image
|
||||||
|
if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then
|
||||||
|
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_boolean "tftp_ready" 1
|
||||||
|
json_close_object
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clone directory images
|
||||||
|
if [ -d "$CLONE_DIR" ]; then
|
||||||
|
for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz; do
|
||||||
|
[ -f "$img" ] || continue
|
||||||
|
name=$(basename "$img")
|
||||||
|
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_boolean "tftp_ready" 0
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_list_tokens() {
|
||||||
|
local tf token created used auto
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_array "tokens"
|
||||||
|
|
||||||
|
if [ -d "$TOKENS_DIR" ]; then
|
||||||
|
for tf in "$TOKENS_DIR"/*.json; do
|
||||||
|
[ -f "$tf" ] || continue
|
||||||
|
token=$(jsonfilter -i "$tf" -e '@.token' 2>/dev/null)
|
||||||
|
created=$(jsonfilter -i "$tf" -e '@.created' 2>/dev/null)
|
||||||
|
used=$(jsonfilter -i "$tf" -e '@.used' 2>/dev/null)
|
||||||
|
auto=$(jsonfilter -i "$tf" -e '@.auto_approve' 2>/dev/null)
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "token" "$token"
|
||||||
|
json_add_string "token_short" "${token:0:16}..."
|
||||||
|
json_add_string "created" "$created"
|
||||||
|
json_add_boolean "used" "$([ "$used" = "true" ] && echo 1 || echo 0)"
|
||||||
|
json_add_boolean "auto_approve" "$([ "$auto" = "true" ] && echo 1 || echo 0)"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_list_clones() {
|
||||||
|
local peer_ip peer_name peer_status
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_array "clones"
|
||||||
|
|
||||||
|
# Get peer list from WireGuard interfaces (most reliable source)
|
||||||
|
# Each wg peer is a potential clone
|
||||||
|
for wg in /etc/config/network; do
|
||||||
|
# Get WireGuard peers from UCI
|
||||||
|
uci -q show network 2>/dev/null | grep "\.public_key=" | while read -r line; do
|
||||||
|
peer_name=$(echo "$line" | cut -d'.' -f2)
|
||||||
|
# Skip if not a wireguard peer
|
||||||
|
echo "$peer_name" | grep -q "^wg" || continue
|
||||||
|
peer_ip=$(uci -q get "network.${peer_name}.endpoint_host" 2>/dev/null)
|
||||||
|
[ -n "$peer_ip" ] || continue
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "info" "$peer_name ($peer_ip)"
|
||||||
|
json_add_string "name" "$peer_name"
|
||||||
|
json_add_string "ip" "$peer_ip"
|
||||||
|
json_add_string "status" "active"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
break # Only need to run once
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also check master-link peer-list if available
|
||||||
|
if [ -x /usr/lib/secubox/master-link.sh ]; then
|
||||||
|
/usr/lib/secubox/master-link.sh peer-list 2>/dev/null | grep "^[0-9]" > /tmp/cloner_peers.tmp 2>/dev/null
|
||||||
|
while read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string "info" "$line"
|
||||||
|
json_add_string "status" "mesh"
|
||||||
|
json_close_object
|
||||||
|
done < /tmp/cloner_peers.tmp 2>/dev/null
|
||||||
|
rm -f /tmp/cloner_peers.tmp
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_generate_token() {
|
||||||
|
local input auto_approve token token_file
|
||||||
|
|
||||||
|
read input
|
||||||
|
auto_approve=$(echo "$input" | jsonfilter -e '@.auto_approve' 2>/dev/null)
|
||||||
|
|
||||||
|
mkdir -p "$TOKENS_DIR"
|
||||||
|
token=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1)
|
||||||
|
token_file="$TOKENS_DIR/${token}.json"
|
||||||
|
|
||||||
|
cat > "$token_file" <<EOF
|
||||||
|
{
|
||||||
|
"token": "$token",
|
||||||
|
"created": "$(date -Iseconds)",
|
||||||
|
"ttl": 86400,
|
||||||
|
"auto_approve": $([ "$auto_approve" = "true" ] && echo "true" || echo "false"),
|
||||||
|
"type": "clone",
|
||||||
|
"used": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "token" "$token"
|
||||||
|
json_add_string "token_short" "${token:0:16}..."
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_build_image() {
|
||||||
|
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"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "secubox-cloner not installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_tftp_start() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
uci -q set dhcp.@dnsmasq[0].enable_tftp='1'
|
||||||
|
uci -q set dhcp.@dnsmasq[0].tftp_root="$TFTP_ROOT"
|
||||||
|
uci commit dhcp
|
||||||
|
/etc/init.d/dnsmasq restart 2>/dev/null
|
||||||
|
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "TFTP server started"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_tftp_stop() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
uci -q set dhcp.@dnsmasq[0].enable_tftp='0'
|
||||||
|
uci commit dhcp
|
||||||
|
/etc/init.d/dnsmasq restart 2>/dev/null
|
||||||
|
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "TFTP server stopped"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_delete_token() {
|
||||||
|
local input token
|
||||||
|
|
||||||
|
read input
|
||||||
|
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ -n "$token" ] && [ -f "$TOKENS_DIR/${token}.json" ]; then
|
||||||
|
rm -f "$TOKENS_DIR/${token}.json"
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Token deleted"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "Token not found"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
do_delete_image() {
|
||||||
|
local input name
|
||||||
|
|
||||||
|
read input
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
|
||||||
|
json_init
|
||||||
|
if [ -n "$name" ]; then
|
||||||
|
rm -f "$CLONE_DIR/$name" "$TFTP_ROOT/$name" 2>/dev/null
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Image deleted"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "message" "Image not found"
|
||||||
|
fi
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
echo '{'
|
||||||
|
echo '"status":{},'
|
||||||
|
echo '"list_images":{},'
|
||||||
|
echo '"list_tokens":{},'
|
||||||
|
echo '"list_clones":{},'
|
||||||
|
echo '"generate_token":{"auto_approve":"Boolean"},'
|
||||||
|
echo '"build_image":{},'
|
||||||
|
echo '"tftp_start":{},'
|
||||||
|
echo '"tftp_stop":{},'
|
||||||
|
echo '"delete_token":{"token":"String"},'
|
||||||
|
echo '"delete_image":{"name":"String"}'
|
||||||
|
echo '}'
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status) do_status ;;
|
||||||
|
list_images) do_list_images ;;
|
||||||
|
list_tokens) do_list_tokens ;;
|
||||||
|
list_clones) do_list_clones ;;
|
||||||
|
generate_token) do_generate_token ;;
|
||||||
|
build_image) do_build_image ;;
|
||||||
|
tftp_start) do_tftp_start ;;
|
||||||
|
tftp_stop) do_tftp_stop ;;
|
||||||
|
delete_token) do_delete_token ;;
|
||||||
|
delete_image) do_delete_image ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"admin/secubox/system/cloner": {
|
||||||
|
"title": "Cloning Station",
|
||||||
|
"order": 50,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "cloner/overview"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-cloner"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"luci-app-cloner": {
|
||||||
|
"description": "Grant access to SecuBox Cloning Station",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.cloner": ["status", "list_images", "list_tokens", "list_clones"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.cloner": ["generate_token", "build_image", "tftp_start", "tftp_stop", "delete_token", "delete_image"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user