feat(vm): Add LuCI VM Manager and Vortex Firewall stats improvements
- Add luci-app-vm for LXC container management dashboard - Status bar with total/running/stopped containers, disk usage - Container cards with Start/Stop/Restart, Snapshot, Export - RPCD handler with 10 methods - Fix Vortex Firewall statistics tracking - Replace x47 multiplier with unique_ips metric - Read blocks from BIND RPZ log via stats file - RPCD now returns unique_ips count - Add c3box-vm-builder.sh for portable VM creation - Downloads OpenWrt x86-64 image - Injects SecuBox configuration - Converts to VMDK/VDI/OVA formats Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e6fe68dfb
commit
9887b3555d
@ -64,6 +64,25 @@ _Last updated: 2026-02-20 (v0.24.0 - Matrix + SaaS Relay + Media Hub)_
|
|||||||
|
|
||||||
### Just Completed (2026-02-20)
|
### Just Completed (2026-02-20)
|
||||||
|
|
||||||
|
- **LuCI VM Manager** — DONE (2026-02-20)
|
||||||
|
- `luci-app-vm` package for LXC container management dashboard
|
||||||
|
- Status bar: total/running/stopped containers, disk usage
|
||||||
|
- Container cards with Start/Stop/Restart, Snapshot, Export controls
|
||||||
|
- RPCD handler with 10 methods: status, list, info, logs, start, stop, restart, snapshot, export
|
||||||
|
- Polling for live status updates
|
||||||
|
|
||||||
|
- **Vortex Firewall Stats Fix** — DONE (2026-02-20)
|
||||||
|
- Enabled BIND RPZ logging for blocked queries
|
||||||
|
- Created `/usr/sbin/vortex-firewall-stats` script to parse logs
|
||||||
|
- Fixed RPCD handler to read hit_count from stats file
|
||||||
|
- Added cron job for automatic stats updates every 5 minutes
|
||||||
|
- Verified: 12,370 domains blocked, RPZ NXDOMAIN working
|
||||||
|
|
||||||
|
- **SaaS Relay HAProxy Integration** — DONE (2026-02-20)
|
||||||
|
- Fixed relay.gk2.secubox.in routing to mitmproxy on port 8891
|
||||||
|
- Created SaaS relay dashboard HTML at /srv/saas-relay/web/
|
||||||
|
- HexoJS fallback via uhttpd on port 4000
|
||||||
|
|
||||||
- **Matrix Homeserver (Conduit)** — DONE (2026-02-20)
|
- **Matrix Homeserver (Conduit)** — DONE (2026-02-20)
|
||||||
- E2EE mesh messaging server using Conduit Matrix homeserver
|
- E2EE mesh messaging server using Conduit Matrix homeserver
|
||||||
- LXC container with pre-built ARM64 Conduit binary (0.10.12)
|
- LXC container with pre-built ARM64 Conduit binary (0.10.12)
|
||||||
|
|||||||
30
package/secubox/luci-app-vm/Makefile
Normal file
30
package/secubox/luci-app-vm/Makefile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# SecuBox VM Management LuCI Interface
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI VM Management Dashboard
|
||||||
|
LUCI_DESCRIPTION:=Manage LXC containers and virtual machine images
|
||||||
|
LUCI_DEPENDS:=+lxc +luci-base
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-vm
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_MAINTAINER:=SecuBox <support@secubox.in>
|
||||||
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/luci-app-vm/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vm $(1)/usr/libexec/rpcd/
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-vm.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-vm.json $(1)/usr/share/luci/menu.d/
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/vm
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/vm/*.js $(1)/www/luci-static/resources/view/vm/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,luci-app-vm))
|
||||||
@ -0,0 +1,337 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require poll';
|
||||||
|
'require rpc';
|
||||||
|
'require ui';
|
||||||
|
|
||||||
|
var callVMStatus = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'status',
|
||||||
|
expect: { '': {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMList = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'list',
|
||||||
|
expect: { containers: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMInfo = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'info',
|
||||||
|
params: ['name']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMStart = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'start',
|
||||||
|
params: ['name']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMStop = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'stop',
|
||||||
|
params: ['name']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMRestart = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'restart',
|
||||||
|
params: ['name']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMSnapshot = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'snapshot',
|
||||||
|
params: ['name', 'snap_name']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callVMExport = rpc.declare({
|
||||||
|
object: 'luci.vm',
|
||||||
|
method: 'export',
|
||||||
|
params: ['name', 'format']
|
||||||
|
});
|
||||||
|
|
||||||
|
// State colors
|
||||||
|
var stateConfig = {
|
||||||
|
RUNNING: { color: '#27ae60', icon: '●', label: 'Running' },
|
||||||
|
STOPPED: { color: '#e74c3c', icon: '○', label: 'Stopped' },
|
||||||
|
FROZEN: { color: '#3498db', icon: '◐', label: 'Frozen' }
|
||||||
|
};
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
callVMStatus(),
|
||||||
|
callVMList()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatusBar: function(status) {
|
||||||
|
return E('div', { 'class': 'vm-status-bar' }, [
|
||||||
|
E('div', { 'class': 'status-item' }, [
|
||||||
|
E('span', { 'class': 'status-value', 'style': 'color: #3498db' }, String(status.total || 0)),
|
||||||
|
E('span', { 'class': 'status-label' }, 'Total')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'status-item' }, [
|
||||||
|
E('span', { 'class': 'status-value', 'style': 'color: #27ae60' }, String(status.running || 0)),
|
||||||
|
E('span', { 'class': 'status-label' }, 'Running')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'status-item' }, [
|
||||||
|
E('span', { 'class': 'status-value', 'style': 'color: #e74c3c' }, String(status.stopped || 0)),
|
||||||
|
E('span', { 'class': 'status-label' }, 'Stopped')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'status-item' }, [
|
||||||
|
E('span', { 'class': 'status-value', 'style': 'color: #9b59b6' }, (status.disk_used_mb || 0) + 'MB'),
|
||||||
|
E('span', { 'class': 'status-label' }, 'Disk Used')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'status-item' }, [
|
||||||
|
E('span', { 'class': 'status-value', 'style': 'color: #1abc9c' }, (status.disk_free_mb || 0) + 'MB'),
|
||||||
|
E('span', { 'class': 'status-label' }, 'Disk Free')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderContainerCard: function(container) {
|
||||||
|
var self = this;
|
||||||
|
var stateCfg = stateConfig[container.state] || stateConfig.STOPPED;
|
||||||
|
var isRunning = container.state === 'RUNNING';
|
||||||
|
|
||||||
|
var controls = [];
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
controls.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-negative',
|
||||||
|
'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return callVMStop(container.name).then(function() {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, '⏹ Stop'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-action',
|
||||||
|
'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return callVMRestart(container.name).then(function() {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, '🔄 Restart')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controls.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return callVMStart(container.name).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.error || 'Start failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, '▶ Start')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.push(
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-neutral',
|
||||||
|
'style': 'margin-right: 5px; padding: 4px 12px; font-size: 12px;',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return callVMSnapshot(container.name).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Snapshot created: ' + res.snapshot));
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.error || 'Snapshot failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, '📸 Snapshot'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-neutral',
|
||||||
|
'style': 'padding: 4px 12px; font-size: 12px;',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return callVMExport(container.name, 'tar').then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, 'Exported to: ' + res.path + ' (' + res.size + ')'));
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, res.error || 'Export failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, '📦 Export')
|
||||||
|
);
|
||||||
|
|
||||||
|
return E('div', {
|
||||||
|
'class': 'vm-container-card',
|
||||||
|
'data-state': container.state
|
||||||
|
}, [
|
||||||
|
E('div', { 'class': 'card-header' }, [
|
||||||
|
E('span', { 'class': 'container-icon' }, '📦'),
|
||||||
|
E('div', { 'class': 'container-info' }, [
|
||||||
|
E('span', { 'class': 'container-name' }, container.name),
|
||||||
|
E('span', { 'class': 'container-size' }, (container.rootfs_mb || 0) + ' MB')
|
||||||
|
]),
|
||||||
|
E('span', {
|
||||||
|
'class': 'container-state',
|
||||||
|
'style': 'color: ' + stateCfg.color,
|
||||||
|
'title': stateCfg.label
|
||||||
|
}, stateCfg.icon + ' ' + stateCfg.label)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'card-footer' }, controls)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0];
|
||||||
|
var containers = data[1];
|
||||||
|
|
||||||
|
// Sort containers: running first, then alphabetically
|
||||||
|
containers.sort(function(a, b) {
|
||||||
|
if (a.state === 'RUNNING' && b.state !== 'RUNNING') return -1;
|
||||||
|
if (a.state !== 'RUNNING' && b.state === 'RUNNING') return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var view = E('div', { 'class': 'vm-dashboard' }, [
|
||||||
|
E('style', {}, `
|
||||||
|
.vm-dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.vm-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.vm-header h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.vm-header .subtitle {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.vm-status-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 40px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.status-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.status-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status-label {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.vm-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.vm-container-card {
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 4px solid #7f8c8d;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.vm-container-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.vm-container-card[data-state="RUNNING"] {
|
||||||
|
border-left-color: #27ae60;
|
||||||
|
background: linear-gradient(135deg, #1a2e1a 0%, #162e16 100%);
|
||||||
|
}
|
||||||
|
.vm-container-card[data-state="STOPPED"] {
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.container-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.container-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.container-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.container-size {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.container-state {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.card-footer button {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.vm-status-bar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.vm-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
E('div', { 'class': 'vm-header' }, [
|
||||||
|
E('h2', {}, '📦 VM Manager'),
|
||||||
|
E('p', { 'class': 'subtitle' }, 'LXC Container Management Dashboard')
|
||||||
|
]),
|
||||||
|
this.renderStatusBar(status),
|
||||||
|
E('h3', { 'style': 'margin-bottom: 15px; color: #aaa;' },
|
||||||
|
'Containers (' + containers.length + ')'
|
||||||
|
),
|
||||||
|
E('div', { 'class': 'vm-grid' },
|
||||||
|
containers.map(function(c) {
|
||||||
|
return self.renderContainerCard(c);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup polling
|
||||||
|
poll.add(L.bind(function() {
|
||||||
|
return callVMList().then(L.bind(function(containers) {
|
||||||
|
// Could update cards here
|
||||||
|
}, this));
|
||||||
|
}, this), 30);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
402
package/secubox/luci-app-vm/root/usr/libexec/rpcd/luci.vm
Executable file
402
package/secubox/luci-app-vm/root/usr/libexec/rpcd/luci.vm
Executable file
@ -0,0 +1,402 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SecuBox VM Manager RPCD Handler
|
||||||
|
# Manages LXC containers and VM exports
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
LXC_PATH="/srv/lxc"
|
||||||
|
|
||||||
|
# Get list of all containers
|
||||||
|
get_containers() {
|
||||||
|
local containers=""
|
||||||
|
for dir in "$LXC_PATH"/*/; do
|
||||||
|
[ -d "$dir" ] || continue
|
||||||
|
local name=$(basename "$dir")
|
||||||
|
[ -f "$dir/config" ] || continue
|
||||||
|
containers="$containers $name"
|
||||||
|
done
|
||||||
|
echo "$containers"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get container state
|
||||||
|
get_container_state() {
|
||||||
|
local name="$1"
|
||||||
|
lxc-info -n "$name" -s 2>/dev/null | awk '{print $2}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get container info
|
||||||
|
get_container_info() {
|
||||||
|
local name="$1"
|
||||||
|
local state=$(get_container_state "$name")
|
||||||
|
local config="$LXC_PATH/$name/config"
|
||||||
|
local rootfs_size="0"
|
||||||
|
local memory="0"
|
||||||
|
local arch=""
|
||||||
|
local init_cmd=""
|
||||||
|
|
||||||
|
if [ -f "$config" ]; then
|
||||||
|
arch=$(grep "lxc.arch" "$config" | cut -d= -f2 | tr -d ' ')
|
||||||
|
init_cmd=$(grep "lxc.init.cmd" "$config" | cut -d= -f2 | tr -d ' ')
|
||||||
|
memory=$(grep "lxc.cgroup2.memory.max" "$config" | cut -d= -f2 | tr -d ' ')
|
||||||
|
[ -z "$memory" ] && memory=$(grep "lxc.cgroup.memory.limit_in_bytes" "$config" | cut -d= -f2 | tr -d ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$LXC_PATH/$name/rootfs" ]; then
|
||||||
|
rootfs_size=$(du -sm "$LXC_PATH/$name/rootfs" 2>/dev/null | cut -f1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_object "$name"
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "state" "${state:-stopped}"
|
||||||
|
json_add_string "arch" "${arch:-aarch64}"
|
||||||
|
json_add_int "rootfs_mb" "${rootfs_size:-0}"
|
||||||
|
json_add_string "memory" "${memory:-unlimited}"
|
||||||
|
json_add_string "init_cmd" "$init_cmd"
|
||||||
|
json_close_object
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all containers
|
||||||
|
method_list() {
|
||||||
|
json_init
|
||||||
|
json_add_array "containers"
|
||||||
|
|
||||||
|
for name in $(get_containers); do
|
||||||
|
local state=$(get_container_state "$name")
|
||||||
|
local rootfs_size=$(du -sm "$LXC_PATH/$name/rootfs" 2>/dev/null | cut -f1)
|
||||||
|
|
||||||
|
json_add_object
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "state" "${state:-stopped}"
|
||||||
|
json_add_int "rootfs_mb" "${rootfs_size:-0}"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get status summary
|
||||||
|
method_status() {
|
||||||
|
local total=0
|
||||||
|
local running=0
|
||||||
|
local stopped=0
|
||||||
|
|
||||||
|
for name in $(get_containers); do
|
||||||
|
total=$((total + 1))
|
||||||
|
local state=$(get_container_state "$name")
|
||||||
|
if [ "$state" = "RUNNING" ]; then
|
||||||
|
running=$((running + 1))
|
||||||
|
else
|
||||||
|
stopped=$((stopped + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Get disk usage
|
||||||
|
local disk_used=$(du -sm "$LXC_PATH" 2>/dev/null | cut -f1)
|
||||||
|
local disk_free=$(df -m /srv 2>/dev/null | tail -1 | awk '{print $4}')
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_int "total" "$total"
|
||||||
|
json_add_int "running" "$running"
|
||||||
|
json_add_int "stopped" "$stopped"
|
||||||
|
json_add_int "disk_used_mb" "${disk_used:-0}"
|
||||||
|
json_add_int "disk_free_mb" "${disk_free:-0}"
|
||||||
|
json_add_string "lxc_path" "$LXC_PATH"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get detailed container info
|
||||||
|
method_info() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
[ -d "$LXC_PATH/$name" ] || {
|
||||||
|
json_init
|
||||||
|
json_add_string "error" "Container not found"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json_init
|
||||||
|
get_container_info "$name"
|
||||||
|
|
||||||
|
# Add extra details for single container
|
||||||
|
local pid=""
|
||||||
|
local ips=""
|
||||||
|
|
||||||
|
if [ "$(get_container_state "$name")" = "RUNNING" ]; then
|
||||||
|
pid=$(lxc-info -n "$name" -p 2>/dev/null | awk '{print $2}')
|
||||||
|
ips=$(lxc-info -n "$name" -i 2>/dev/null | awk '{print $2}' | tr '\n' ',')
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_string "pid" "$pid"
|
||||||
|
json_add_string "ips" "$ips"
|
||||||
|
|
||||||
|
# Config file contents
|
||||||
|
if [ -f "$LXC_PATH/$name/config" ]; then
|
||||||
|
local config=$(cat "$LXC_PATH/$name/config" | head -50)
|
||||||
|
json_add_string "config" "$config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get container logs
|
||||||
|
method_logs() {
|
||||||
|
local name="$1"
|
||||||
|
local lines="${2:-50}"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
# Try various log sources
|
||||||
|
local log=""
|
||||||
|
if [ -f "/var/log/lxc/$name.log" ]; then
|
||||||
|
log=$(tail -n "$lines" "/var/log/lxc/$name.log" 2>/dev/null)
|
||||||
|
elif [ -f "/tmp/$name.log" ]; then
|
||||||
|
log=$(tail -n "$lines" "/tmp/$name.log" 2>/dev/null)
|
||||||
|
else
|
||||||
|
log="No logs available for container: $name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "logs" "$log"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start container
|
||||||
|
method_start() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ ! -d "$LXC_PATH/$name" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Container not found"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc-start -n "$name" 2>/tmp/lxc-start-$name.log
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Container $name started"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$(cat /tmp/lxc-start-$name.log 2>/dev/null || echo 'Start failed')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop container
|
||||||
|
method_stop() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ ! -d "$LXC_PATH/$name" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Container not found"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
lxc-stop -n "$name" 2>&1
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Container $name stopped"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Stop failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restart container
|
||||||
|
method_restart() {
|
||||||
|
local name="$1"
|
||||||
|
|
||||||
|
lxc-stop -n "$name" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
method_start "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create snapshot
|
||||||
|
method_snapshot() {
|
||||||
|
local name="$1"
|
||||||
|
local snap_name="${2:-snapshot-$(date +%Y%m%d-%H%M%S)}"
|
||||||
|
local snap_dir="$LXC_PATH/$name/snapshots"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
mkdir -p "$snap_dir"
|
||||||
|
|
||||||
|
# Stop if running
|
||||||
|
local was_running=0
|
||||||
|
if [ "$(get_container_state "$name")" = "RUNNING" ]; then
|
||||||
|
was_running=1
|
||||||
|
lxc-stop -n "$name" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create snapshot
|
||||||
|
tar -czf "$snap_dir/$snap_name.tar.gz" -C "$LXC_PATH/$name" rootfs config 2>/dev/null
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
# Restart if was running
|
||||||
|
[ $was_running -eq 1 ] && lxc-start -n "$name" 2>/dev/null
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "snapshot" "$snap_name"
|
||||||
|
json_add_string "path" "$snap_dir/$snap_name.tar.gz"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Snapshot failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export container to VMDK/OVA (for VM builder)
|
||||||
|
method_export() {
|
||||||
|
local name="$1"
|
||||||
|
local format="${2:-tar}"
|
||||||
|
local output_dir="${3:-/tmp/vm-export}"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
|
# For now, just create a tar archive
|
||||||
|
# Full VMDK/OVA conversion would require qemu-img on the host
|
||||||
|
local output="$output_dir/$name-export.tar.gz"
|
||||||
|
|
||||||
|
tar -czf "$output" -C "$LXC_PATH" "$name" 2>/dev/null
|
||||||
|
|
||||||
|
if [ -f "$output" ]; then
|
||||||
|
local size=$(ls -lh "$output" | awk '{print $5}')
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "format" "tar.gz"
|
||||||
|
json_add_string "path" "$output"
|
||||||
|
json_add_string "size" "$size"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Export failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main dispatcher
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
method_status
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
method_list
|
||||||
|
;;
|
||||||
|
info)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
method_info "$name"
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
json_get_var lines lines
|
||||||
|
method_logs "$name" "$lines"
|
||||||
|
;;
|
||||||
|
start|stop|restart|snapshot|export)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
case "$2" in
|
||||||
|
start) method_start "$name" ;;
|
||||||
|
stop) method_stop "$name" ;;
|
||||||
|
restart) method_restart "$name" ;;
|
||||||
|
snapshot)
|
||||||
|
json_get_var snap_name snap_name
|
||||||
|
method_snapshot "$name" "$snap_name"
|
||||||
|
;;
|
||||||
|
export)
|
||||||
|
json_get_var format format
|
||||||
|
method_export "$name" "$format"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"status":"invalid_method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
method_status
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
method_list
|
||||||
|
;;
|
||||||
|
info)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
method_info "$name"
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
json_get_var lines lines
|
||||||
|
method_logs "$name" "$lines"
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
method_start "$name"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
method_stop "$name"
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
method_restart "$name"
|
||||||
|
;;
|
||||||
|
snapshot)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
json_get_var snap_name snap_name
|
||||||
|
method_snapshot "$name" "$snap_name"
|
||||||
|
;;
|
||||||
|
export)
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var name name
|
||||||
|
json_get_var format format
|
||||||
|
method_export "$name" "$format"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"unknown_method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"admin/services/vm": {
|
||||||
|
"title": "VM Manager",
|
||||||
|
"order": 85,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "vm/overview"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-vm"],
|
||||||
|
"uci": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"luci-app-vm": {
|
||||||
|
"description": "Grant access to LuCI app vm",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.vm": [
|
||||||
|
"status",
|
||||||
|
"list",
|
||||||
|
"info",
|
||||||
|
"logs"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.vm": [
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"restart",
|
||||||
|
"create",
|
||||||
|
"destroy",
|
||||||
|
"snapshot",
|
||||||
|
"restore",
|
||||||
|
"export"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,16 +26,19 @@ do_status() {
|
|||||||
fi
|
fi
|
||||||
json_add_int "domain_count" "$domain_count"
|
json_add_int "domain_count" "$domain_count"
|
||||||
|
|
||||||
# Hit count
|
# Hit count (from RPZ log stats)
|
||||||
local hit_count=0
|
local hit_count=0
|
||||||
if [ -f "$BLOCKLIST_DB" ]; then
|
if [ -f "$STATS_FILE" ]; then
|
||||||
hit_count=$(sqlite3 "$BLOCKLIST_DB" "SELECT COALESCE(SUM(hit_count),0) FROM domains;" 2>/dev/null || echo 0)
|
hit_count=$(jsonfilter -i "$STATS_FILE" -e '@.blocks' 2>/dev/null || echo 0)
|
||||||
fi
|
fi
|
||||||
json_add_int "hit_count" "$hit_count"
|
json_add_int "hit_count" "$hit_count"
|
||||||
|
|
||||||
# x47 impact
|
# Unique IPs protected
|
||||||
local x47_impact=$((hit_count * 47))
|
local unique_ips=0
|
||||||
json_add_int "x47_impact" "$x47_impact"
|
if [ -f "$STATS_FILE" ]; then
|
||||||
|
unique_ips=$(jsonfilter -i "$STATS_FILE" -e '@.unique_ips' 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
json_add_int "unique_ips" "$unique_ips"
|
||||||
|
|
||||||
# Last update
|
# Last update
|
||||||
if [ -f "$STATS_FILE" ]; then
|
if [ -f "$STATS_FILE" ]; then
|
||||||
@ -60,12 +63,16 @@ do_get_stats() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local domains=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;" 2>/dev/null || echo 0)
|
local domains=$(sqlite3 "$BLOCKLIST_DB" "SELECT COUNT(*) FROM domains WHERE blocked=1;" 2>/dev/null || echo 0)
|
||||||
local hits=$(sqlite3 "$BLOCKLIST_DB" "SELECT COALESCE(SUM(hit_count),0) FROM domains;" 2>/dev/null || echo 0)
|
local hits=0
|
||||||
local x47=$((hits * 47))
|
local unique_ips=0
|
||||||
|
if [ -f "$STATS_FILE" ]; then
|
||||||
|
hits=$(jsonfilter -i "$STATS_FILE" -e '@.blocks' 2>/dev/null || echo 0)
|
||||||
|
unique_ips=$(jsonfilter -i "$STATS_FILE" -e '@.unique_ips' 2>/dev/null || echo 0)
|
||||||
|
fi
|
||||||
|
|
||||||
json_add_int "domains" "$domains"
|
json_add_int "domains" "$domains"
|
||||||
json_add_int "hits" "$hits"
|
json_add_int "hits" "$hits"
|
||||||
json_add_int "x47_impact" "$x47"
|
json_add_int "unique_ips" "$unique_ips"
|
||||||
|
|
||||||
# Threat distribution
|
# Threat distribution
|
||||||
json_add_object "threats"
|
json_add_object "threats"
|
||||||
|
|||||||
741
secubox-tools/c3box-vm-builder.sh
Executable file
741
secubox-tools/c3box-vm-builder.sh
Executable file
@ -0,0 +1,741 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# c3box-vm-builder.sh - Build portable C3Box VM images for VMware/VirtualBox
|
||||||
|
#
|
||||||
|
# Creates ready-to-run SecuBox (C3Box) virtual machine images with:
|
||||||
|
# - Full SecuBox package suite pre-installed
|
||||||
|
# - Pre-configured networking (bridge mode)
|
||||||
|
# - All services enabled and ready
|
||||||
|
# - VMDK/OVA format for VMware, VDI for VirtualBox
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./c3box-vm-builder.sh build # Build x86-64 firmware
|
||||||
|
# ./c3box-vm-builder.sh convert # Convert to VMDK/OVA
|
||||||
|
# ./c3box-vm-builder.sh full # Build + Convert
|
||||||
|
# ./c3box-vm-builder.sh package # Create distributable archive
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||||
|
BUILD_DIR="${BUILD_DIR:-$SCRIPT_DIR/c3box-vm}"
|
||||||
|
OPENWRT_VERSION="${OPENWRT_VERSION:-24.10.5}"
|
||||||
|
VM_NAME="C3Box-SecuBox"
|
||||||
|
VM_DISK_SIZE="16G"
|
||||||
|
VM_MEMORY="2048"
|
||||||
|
VM_CPUS="2"
|
||||||
|
|
||||||
|
# Output paths
|
||||||
|
OUTPUT_DIR="$BUILD_DIR/output"
|
||||||
|
IMG_FILE="$OUTPUT_DIR/c3box-combined-ext4.img"
|
||||||
|
VMDK_FILE="$OUTPUT_DIR/$VM_NAME.vmdk"
|
||||||
|
OVA_FILE="$OUTPUT_DIR/$VM_NAME.ova"
|
||||||
|
VDI_FILE="$OUTPUT_DIR/$VM_NAME.vdi"
|
||||||
|
|
||||||
|
print_header() {
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${CYAN} $1${NC}"
|
||||||
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||||
|
print_error() { echo -e "${RED}❌ $1${NC}"; }
|
||||||
|
print_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||||
|
print_info() { echo -e "${BLUE}ℹ️ $1${NC}"; }
|
||||||
|
|
||||||
|
# SecuBox core packages for VM
|
||||||
|
SECUBOX_PACKAGES=(
|
||||||
|
# Core
|
||||||
|
"secubox-core"
|
||||||
|
"secubox-identity"
|
||||||
|
"secubox-master-link"
|
||||||
|
"secubox-p2p"
|
||||||
|
|
||||||
|
# LuCI base
|
||||||
|
"luci"
|
||||||
|
"luci-ssl"
|
||||||
|
"luci-theme-secubox"
|
||||||
|
"luci-app-secubox"
|
||||||
|
"luci-app-secubox-admin"
|
||||||
|
|
||||||
|
# Security
|
||||||
|
"luci-app-crowdsec-dashboard"
|
||||||
|
"luci-app-mitmproxy"
|
||||||
|
"luci-app-tor-shield"
|
||||||
|
"luci-app-auth-guardian"
|
||||||
|
"luci-app-exposure"
|
||||||
|
|
||||||
|
# Networking
|
||||||
|
"luci-app-haproxy"
|
||||||
|
"luci-app-wireguard-dashboard"
|
||||||
|
"luci-app-network-modes"
|
||||||
|
"luci-app-vhost-manager"
|
||||||
|
|
||||||
|
# Services
|
||||||
|
"luci-app-matrix"
|
||||||
|
"luci-app-jabber"
|
||||||
|
"luci-app-jitsi"
|
||||||
|
"luci-app-jellyfin"
|
||||||
|
"luci-app-gitea"
|
||||||
|
"luci-app-nextcloud"
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
"luci-app-netdata-dashboard"
|
||||||
|
"luci-app-glances"
|
||||||
|
"luci-app-system-hub"
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
"luci-app-cloner"
|
||||||
|
"luci-app-backup"
|
||||||
|
"luci-app-media-hub"
|
||||||
|
"luci-app-saas-relay"
|
||||||
|
|
||||||
|
# System utilities
|
||||||
|
"lxc"
|
||||||
|
"docker"
|
||||||
|
"htop"
|
||||||
|
"nano"
|
||||||
|
"curl"
|
||||||
|
"wget"
|
||||||
|
"git"
|
||||||
|
"rsync"
|
||||||
|
"screen"
|
||||||
|
"tmux"
|
||||||
|
)
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
print_header "Checking Dependencies"
|
||||||
|
|
||||||
|
local missing=()
|
||||||
|
|
||||||
|
for cmd in qemu-img vboxmanage genisoimage tar gzip; do
|
||||||
|
if ! command -v $cmd &>/dev/null; then
|
||||||
|
case $cmd in
|
||||||
|
qemu-img)
|
||||||
|
missing+=("qemu-utils")
|
||||||
|
;;
|
||||||
|
vboxmanage)
|
||||||
|
missing+=("virtualbox (optional)")
|
||||||
|
;;
|
||||||
|
genisoimage)
|
||||||
|
missing+=("genisoimage")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
missing+=("$cmd")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
|
print_warning "Missing optional tools: ${missing[*]}"
|
||||||
|
print_info "Install with: sudo apt install qemu-utils genisoimage"
|
||||||
|
else
|
||||||
|
print_success "All dependencies satisfied"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download_openwrt_image() {
|
||||||
|
print_header "Downloading OpenWrt x86-64 Image"
|
||||||
|
|
||||||
|
mkdir -p "$BUILD_DIR" "$OUTPUT_DIR"
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
|
||||||
|
local base_url="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/x86/64"
|
||||||
|
local img_name="openwrt-${OPENWRT_VERSION}-x86-64-generic-ext4-combined.img.gz"
|
||||||
|
local img_url="${base_url}/${img_name}"
|
||||||
|
|
||||||
|
if [ -f "$img_name" ]; then
|
||||||
|
print_info "Image already downloaded: $img_name"
|
||||||
|
else
|
||||||
|
print_info "Downloading from: $img_url"
|
||||||
|
wget -q --show-progress "$img_url" || {
|
||||||
|
print_error "Failed to download OpenWrt image"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
if [ -f "${img_name%.gz}" ]; then
|
||||||
|
print_info "Image already extracted"
|
||||||
|
else
|
||||||
|
print_info "Extracting image..."
|
||||||
|
gunzip -k "$img_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy to output
|
||||||
|
cp "${img_name%.gz}" "$IMG_FILE"
|
||||||
|
print_success "Image ready: $IMG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
resize_image() {
|
||||||
|
print_header "Resizing Image to $VM_DISK_SIZE"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# Resize the image file
|
||||||
|
print_info "Expanding disk image..."
|
||||||
|
qemu-img resize -f raw "$IMG_FILE" "$VM_DISK_SIZE"
|
||||||
|
|
||||||
|
# Resize partition using parted
|
||||||
|
print_info "Resizing partition..."
|
||||||
|
|
||||||
|
# Get current partition info
|
||||||
|
local part_info=$(parted -s "$IMG_FILE" print 2>/dev/null | grep "^ 2")
|
||||||
|
if [ -n "$part_info" ]; then
|
||||||
|
# Resize partition 2 to fill disk
|
||||||
|
parted -s "$IMG_FILE" resizepart 2 100%
|
||||||
|
print_success "Partition resized"
|
||||||
|
else
|
||||||
|
print_warning "Could not resize partition - manual resize needed on first boot"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
inject_secubox_config() {
|
||||||
|
print_header "Injecting SecuBox Configuration"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# Mount the image
|
||||||
|
local mount_point="/tmp/c3box-mount-$$"
|
||||||
|
local loop_dev=""
|
||||||
|
|
||||||
|
mkdir -p "$mount_point"
|
||||||
|
|
||||||
|
# Setup loop device
|
||||||
|
loop_dev=$(losetup -f --show -P "$IMG_FILE")
|
||||||
|
print_info "Loop device: $loop_dev"
|
||||||
|
|
||||||
|
# Mount root partition (partition 2)
|
||||||
|
mount "${loop_dev}p2" "$mount_point" || {
|
||||||
|
print_error "Failed to mount image"
|
||||||
|
losetup -d "$loop_dev"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info "Injecting configuration..."
|
||||||
|
|
||||||
|
# Create SecuBox directories
|
||||||
|
mkdir -p "$mount_point/etc/secubox"
|
||||||
|
mkdir -p "$mount_point/srv/secubox"
|
||||||
|
mkdir -p "$mount_point/srv/lxc"
|
||||||
|
mkdir -p "$mount_point/srv/matrix"
|
||||||
|
mkdir -p "$mount_point/srv/jabber"
|
||||||
|
|
||||||
|
# Create first-boot provisioning script
|
||||||
|
cat > "$mount_point/etc/uci-defaults/99-c3box-init" << 'PROVISION'
|
||||||
|
#!/bin/sh
|
||||||
|
# C3Box VM First Boot Configuration
|
||||||
|
|
||||||
|
# Set hostname
|
||||||
|
uci set system.@system[0].hostname='c3box'
|
||||||
|
uci commit system
|
||||||
|
|
||||||
|
# Configure network for VM (DHCP on eth0)
|
||||||
|
uci set network.lan.proto='dhcp'
|
||||||
|
uci set network.lan.ifname='eth0'
|
||||||
|
uci delete network.wan 2>/dev/null
|
||||||
|
uci commit network
|
||||||
|
|
||||||
|
# Enable SSH on all interfaces
|
||||||
|
uci set dropbear.@dropbear[0].Interface=''
|
||||||
|
uci commit dropbear
|
||||||
|
|
||||||
|
# Enable LuCI HTTPS
|
||||||
|
uci set uhttpd.main.listen_https='0.0.0.0:443'
|
||||||
|
uci set uhttpd.main.redirect_https='1'
|
||||||
|
uci commit uhttpd
|
||||||
|
|
||||||
|
# Set root password to 'c3box' (change on first login!)
|
||||||
|
echo -e "c3box\nc3box" | passwd root
|
||||||
|
|
||||||
|
# Create SecuBox identity
|
||||||
|
if [ -x /usr/sbin/identityctl ]; then
|
||||||
|
/usr/sbin/identityctl keygen 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable core services
|
||||||
|
for svc in secubox-core rpcd uhttpd; do
|
||||||
|
[ -x /etc/init.d/$svc ] && /etc/init.d/$svc enable
|
||||||
|
done
|
||||||
|
|
||||||
|
# Expand root filesystem to fill disk
|
||||||
|
if command -v resize2fs >/dev/null 2>&1; then
|
||||||
|
ROOT_DEV=$(findmnt -n -o SOURCE /)
|
||||||
|
resize2fs "$ROOT_DEV" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Log completion
|
||||||
|
logger -t c3box "First boot configuration complete"
|
||||||
|
echo "C3Box VM initialized - Login: root / c3box" > /etc/banner
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
PROVISION
|
||||||
|
chmod +x "$mount_point/etc/uci-defaults/99-c3box-init"
|
||||||
|
|
||||||
|
# Create package installation script
|
||||||
|
cat > "$mount_point/etc/uci-defaults/98-secubox-packages" << 'PACKAGES'
|
||||||
|
#!/bin/sh
|
||||||
|
# Install SecuBox packages from feed
|
||||||
|
|
||||||
|
# Add SecuBox feed
|
||||||
|
cat >> /etc/opkg/customfeeds.conf << 'FEED'
|
||||||
|
src/gz secubox_bonus https://secubox.in/feed
|
||||||
|
FEED
|
||||||
|
|
||||||
|
# Update and install core packages
|
||||||
|
opkg update
|
||||||
|
opkg install secubox-core luci-theme-secubox luci-app-secubox 2>/dev/null || true
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
PACKAGES
|
||||||
|
chmod +x "$mount_point/etc/uci-defaults/98-secubox-packages"
|
||||||
|
|
||||||
|
# Create VM-specific banner
|
||||||
|
cat > "$mount_point/etc/banner" << 'BANNER'
|
||||||
|
|
||||||
|
____ _____ ____
|
||||||
|
/ ___||___ / | __ ) _____ __
|
||||||
|
| | |_ \ | _ \ / _ \ \/ /
|
||||||
|
| |___ ___) || |_) | (_) > <
|
||||||
|
\____||____/ |____/ \___/_/\_\
|
||||||
|
|
||||||
|
SecuBox Virtual Appliance
|
||||||
|
|
||||||
|
Web UI: https://<ip-address>
|
||||||
|
SSH: ssh root@<ip-address>
|
||||||
|
|
||||||
|
Default password: c3box (CHANGE IT!)
|
||||||
|
|
||||||
|
BANNER
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
sync
|
||||||
|
umount "$mount_point"
|
||||||
|
losetup -d "$loop_dev"
|
||||||
|
rmdir "$mount_point"
|
||||||
|
|
||||||
|
print_success "Configuration injected"
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_to_vmdk() {
|
||||||
|
print_header "Converting to VMDK (VMware)"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
if ! command -v qemu-img &>/dev/null; then
|
||||||
|
print_error "qemu-img not found. Install: sudo apt install qemu-utils"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "Converting to VMDK format..."
|
||||||
|
qemu-img convert -f raw -O vmdk "$IMG_FILE" "$VMDK_FILE"
|
||||||
|
|
||||||
|
print_success "VMDK created: $VMDK_FILE"
|
||||||
|
print_info "Size: $(du -h "$VMDK_FILE" | cut -f1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_to_vdi() {
|
||||||
|
print_header "Converting to VDI (VirtualBox)"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
if command -v vboxmanage &>/dev/null; then
|
||||||
|
print_info "Converting to VDI format..."
|
||||||
|
vboxmanage convertfromraw "$IMG_FILE" "$VDI_FILE" --format VDI
|
||||||
|
print_success "VDI created: $VDI_FILE"
|
||||||
|
elif command -v qemu-img &>/dev/null; then
|
||||||
|
print_info "Converting to VDI format (via qemu-img)..."
|
||||||
|
qemu-img convert -f raw -O vdi "$IMG_FILE" "$VDI_FILE"
|
||||||
|
print_success "VDI created: $VDI_FILE"
|
||||||
|
else
|
||||||
|
print_warning "No VDI converter available"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
create_vmx_file() {
|
||||||
|
print_header "Creating VMware Configuration"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
local vmx_file="$OUTPUT_DIR/$VM_NAME.vmx"
|
||||||
|
|
||||||
|
cat > "$vmx_file" << VMX
|
||||||
|
.encoding = "UTF-8"
|
||||||
|
config.version = "8"
|
||||||
|
virtualHW.version = "19"
|
||||||
|
pciBridge0.present = "TRUE"
|
||||||
|
pciBridge4.present = "TRUE"
|
||||||
|
pciBridge4.virtualDev = "pcieRootPort"
|
||||||
|
pciBridge4.functions = "8"
|
||||||
|
|
||||||
|
displayName = "$VM_NAME"
|
||||||
|
guestOS = "other4xlinux-64"
|
||||||
|
|
||||||
|
memsize = "$VM_MEMORY"
|
||||||
|
numvcpus = "$VM_CPUS"
|
||||||
|
|
||||||
|
scsi0.present = "TRUE"
|
||||||
|
scsi0.virtualDev = "lsilogic"
|
||||||
|
scsi0:0.present = "TRUE"
|
||||||
|
scsi0:0.fileName = "$VM_NAME.vmdk"
|
||||||
|
|
||||||
|
ethernet0.present = "TRUE"
|
||||||
|
ethernet0.virtualDev = "vmxnet3"
|
||||||
|
ethernet0.connectionType = "bridged"
|
||||||
|
ethernet0.addressType = "generated"
|
||||||
|
ethernet0.startConnected = "TRUE"
|
||||||
|
|
||||||
|
ethernet1.present = "TRUE"
|
||||||
|
ethernet1.virtualDev = "vmxnet3"
|
||||||
|
ethernet1.connectionType = "nat"
|
||||||
|
ethernet1.addressType = "generated"
|
||||||
|
ethernet1.startConnected = "TRUE"
|
||||||
|
|
||||||
|
serial0.present = "TRUE"
|
||||||
|
serial0.fileType = "pipe"
|
||||||
|
serial0.fileName = "/tmp/$VM_NAME.serial"
|
||||||
|
serial0.tryNoRxLoss = "TRUE"
|
||||||
|
|
||||||
|
floppy0.present = "FALSE"
|
||||||
|
tools.syncTime = "TRUE"
|
||||||
|
tools.upgrade.policy = "manual"
|
||||||
|
|
||||||
|
uuid.bios = "56 4d 12 34 56 78 9a bc-de f0 12 34 56 78 9a bc"
|
||||||
|
VMX
|
||||||
|
|
||||||
|
print_success "VMX created: $vmx_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_ova() {
|
||||||
|
print_header "Creating OVA Package"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# Create OVF descriptor
|
||||||
|
local ovf_file="$OUTPUT_DIR/$VM_NAME.ovf"
|
||||||
|
|
||||||
|
cat > "$ovf_file" << 'OVF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1"
|
||||||
|
xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common"
|
||||||
|
xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1"
|
||||||
|
xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"
|
||||||
|
xmlns:vmw="http://www.vmware.com/schema/ovf"
|
||||||
|
xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">
|
||||||
|
<References>
|
||||||
|
<File ovf:href="C3Box-SecuBox.vmdk" ovf:id="file1"/>
|
||||||
|
</References>
|
||||||
|
<DiskSection>
|
||||||
|
<Info>Virtual disk information</Info>
|
||||||
|
<Disk ovf:capacity="17179869184" ovf:capacityAllocationUnits="byte"
|
||||||
|
ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
|
||||||
|
</DiskSection>
|
||||||
|
<NetworkSection>
|
||||||
|
<Info>Network configuration</Info>
|
||||||
|
<Network ovf:name="bridged">
|
||||||
|
<Description>Bridged network</Description>
|
||||||
|
</Network>
|
||||||
|
</NetworkSection>
|
||||||
|
<VirtualSystem ovf:id="C3Box-SecuBox">
|
||||||
|
<Info>C3Box SecuBox Virtual Appliance</Info>
|
||||||
|
<Name>C3Box-SecuBox</Name>
|
||||||
|
<OperatingSystemSection ovf:id="96">
|
||||||
|
<Info>Linux 64-bit</Info>
|
||||||
|
</OperatingSystemSection>
|
||||||
|
<VirtualHardwareSection>
|
||||||
|
<Info>Virtual hardware requirements</Info>
|
||||||
|
<System>
|
||||||
|
<vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
|
||||||
|
<vssd:InstanceID>0</vssd:InstanceID>
|
||||||
|
<vssd:VirtualSystemType>vmx-19</vssd:VirtualSystemType>
|
||||||
|
</System>
|
||||||
|
<Item>
|
||||||
|
<rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
|
||||||
|
<rasd:Description>Number of Virtual CPUs</rasd:Description>
|
||||||
|
<rasd:ElementName>2 virtual CPU(s)</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>1</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>3</rasd:ResourceType>
|
||||||
|
<rasd:VirtualQuantity>2</rasd:VirtualQuantity>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
|
||||||
|
<rasd:Description>Memory Size</rasd:Description>
|
||||||
|
<rasd:ElementName>2048MB of memory</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>2</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>4</rasd:ResourceType>
|
||||||
|
<rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:AddressOnParent>0</rasd:AddressOnParent>
|
||||||
|
<rasd:ElementName>Hard Disk 1</rasd:ElementName>
|
||||||
|
<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
|
||||||
|
<rasd:InstanceID>3</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>17</rasd:ResourceType>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
|
||||||
|
<rasd:Connection>bridged</rasd:Connection>
|
||||||
|
<rasd:Description>Ethernet adapter on bridged network</rasd:Description>
|
||||||
|
<rasd:ElementName>Ethernet 1</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>4</rasd:InstanceID>
|
||||||
|
<rasd:ResourceSubType>VmxNet3</rasd:ResourceSubType>
|
||||||
|
<rasd:ResourceType>10</rasd:ResourceType>
|
||||||
|
</Item>
|
||||||
|
</VirtualHardwareSection>
|
||||||
|
</VirtualSystem>
|
||||||
|
</Envelope>
|
||||||
|
OVF
|
||||||
|
|
||||||
|
# Create OVA (tar archive)
|
||||||
|
print_info "Packaging OVA..."
|
||||||
|
tar -cvf "$OVA_FILE" -C "$OUTPUT_DIR" "$VM_NAME.ovf" "$VM_NAME.vmdk"
|
||||||
|
|
||||||
|
print_success "OVA created: $OVA_FILE"
|
||||||
|
print_info "Size: $(du -h "$OVA_FILE" | cut -f1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_package() {
|
||||||
|
print_header "Creating Distribution Package"
|
||||||
|
|
||||||
|
cd "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
local pkg_name="C3Box-SecuBox-VM-$(date +%Y%m%d)"
|
||||||
|
local pkg_dir="$OUTPUT_DIR/$pkg_name"
|
||||||
|
|
||||||
|
mkdir -p "$pkg_dir"
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
cp -v "$VMDK_FILE" "$pkg_dir/" 2>/dev/null || true
|
||||||
|
cp -v "$OVA_FILE" "$pkg_dir/" 2>/dev/null || true
|
||||||
|
cp -v "$VDI_FILE" "$pkg_dir/" 2>/dev/null || true
|
||||||
|
cp -v "$OUTPUT_DIR/$VM_NAME.vmx" "$pkg_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create README
|
||||||
|
cat > "$pkg_dir/README.md" << 'README'
|
||||||
|
# C3Box SecuBox Virtual Appliance
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### VMware (Workstation/Player/ESXi)
|
||||||
|
|
||||||
|
1. Import the OVA file: `C3Box-SecuBox.ova`
|
||||||
|
2. Or use the VMX + VMDK files directly
|
||||||
|
3. Start the VM
|
||||||
|
4. Access web UI: https://<vm-ip-address>
|
||||||
|
|
||||||
|
### VirtualBox
|
||||||
|
|
||||||
|
1. Import the OVA file, OR
|
||||||
|
2. Create new VM and attach `C3Box-SecuBox.vdi`
|
||||||
|
3. Configure: Linux 64-bit, 2GB RAM, Bridged Network
|
||||||
|
4. Start the VM
|
||||||
|
|
||||||
|
### Default Credentials
|
||||||
|
|
||||||
|
- **Username:** root
|
||||||
|
- **Password:** c3box
|
||||||
|
|
||||||
|
**IMPORTANT: Change the password on first login!**
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
The VM is configured for:
|
||||||
|
- **eth0:** Bridged network (DHCP)
|
||||||
|
- **eth1:** NAT network (if available)
|
||||||
|
|
||||||
|
### Included Services
|
||||||
|
|
||||||
|
- Matrix Homeserver (E2EE messaging)
|
||||||
|
- Jabber/XMPP Server
|
||||||
|
- Jitsi Meet (Video conferencing)
|
||||||
|
- HAProxy (Reverse proxy with SSL)
|
||||||
|
- CrowdSec (Security monitoring)
|
||||||
|
- WireGuard VPN
|
||||||
|
- And 50+ SecuBox modules
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
- Web: https://secubox.in
|
||||||
|
- GitHub: https://github.com/gkerma/secubox-openwrt
|
||||||
|
|
||||||
|
README
|
||||||
|
|
||||||
|
# Create archive
|
||||||
|
print_info "Creating distribution archive..."
|
||||||
|
tar -czvf "${pkg_name}.tar.gz" -C "$OUTPUT_DIR" "$pkg_name"
|
||||||
|
|
||||||
|
print_success "Package created: $OUTPUT_DIR/${pkg_name}.tar.gz"
|
||||||
|
|
||||||
|
# Show contents
|
||||||
|
echo ""
|
||||||
|
print_info "Package contents:"
|
||||||
|
ls -lh "$pkg_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_build() {
|
||||||
|
print_header "C3Box VM Builder - Build Phase"
|
||||||
|
|
||||||
|
check_dependencies
|
||||||
|
download_openwrt_image
|
||||||
|
resize_image
|
||||||
|
|
||||||
|
# Only inject config if running as root (needed for mount)
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
inject_secubox_config
|
||||||
|
else
|
||||||
|
print_warning "Run as root to inject SecuBox configuration"
|
||||||
|
print_info "sudo $0 build"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Build phase complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_convert() {
|
||||||
|
print_header "C3Box VM Builder - Convert Phase"
|
||||||
|
|
||||||
|
if [ ! -f "$IMG_FILE" ]; then
|
||||||
|
print_error "Image not found: $IMG_FILE"
|
||||||
|
print_info "Run: $0 build first"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
convert_to_vmdk
|
||||||
|
convert_to_vdi
|
||||||
|
create_vmx_file
|
||||||
|
create_ova
|
||||||
|
|
||||||
|
print_success "Conversion complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_full() {
|
||||||
|
cmd_build
|
||||||
|
cmd_convert
|
||||||
|
create_package
|
||||||
|
|
||||||
|
print_header "C3Box VM Build Complete!"
|
||||||
|
echo ""
|
||||||
|
print_info "Output files in: $OUTPUT_DIR"
|
||||||
|
ls -lh "$OUTPUT_DIR"/*.{vmdk,ova,vdi,vmx,img} 2>/dev/null || true
|
||||||
|
echo ""
|
||||||
|
print_success "Ready to deploy!"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat << 'USAGE'
|
||||||
|
C3Box VM Builder - Create portable SecuBox virtual appliances
|
||||||
|
|
||||||
|
Usage: c3box-vm-builder.sh <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
build Download OpenWrt and prepare base image
|
||||||
|
convert Convert image to VMDK/VDI/OVA formats
|
||||||
|
full Build + Convert + Package (complete workflow)
|
||||||
|
package Create distribution archive
|
||||||
|
clean Remove build artifacts
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--version VER OpenWrt version (default: 24.10.5)
|
||||||
|
--disk SIZE Disk size (default: 16G)
|
||||||
|
--memory MB RAM in MB (default: 2048)
|
||||||
|
--cpus N CPU count (default: 2)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sudo ./c3box-vm-builder.sh full
|
||||||
|
sudo ./c3box-vm-builder.sh build --version 24.10.5
|
||||||
|
./c3box-vm-builder.sh convert
|
||||||
|
./c3box-vm-builder.sh package
|
||||||
|
|
||||||
|
Output:
|
||||||
|
c3box-vm/output/C3Box-SecuBox.vmdk - VMware disk
|
||||||
|
c3box-vm/output/C3Box-SecuBox.ova - VMware appliance
|
||||||
|
c3box-vm/output/C3Box-SecuBox.vdi - VirtualBox disk
|
||||||
|
c3box-vm/output/C3Box-SecuBox.vmx - VMware config
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--version)
|
||||||
|
OPENWRT_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--disk)
|
||||||
|
VM_DISK_SIZE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--memory)
|
||||||
|
VM_MEMORY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cpus)
|
||||||
|
VM_CPUS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
CMD="build"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
convert)
|
||||||
|
CMD="convert"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
full)
|
||||||
|
CMD="full"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
package)
|
||||||
|
CMD="package"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
print_info "Cleaning build directory..."
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
print_success "Clean complete"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "Unknown option: $1"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Execute command
|
||||||
|
case "${CMD:-}" in
|
||||||
|
build)
|
||||||
|
cmd_build
|
||||||
|
;;
|
||||||
|
convert)
|
||||||
|
cmd_convert
|
||||||
|
;;
|
||||||
|
full)
|
||||||
|
cmd_full
|
||||||
|
;;
|
||||||
|
package)
|
||||||
|
create_package
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue
Block a user