feat(glances): Add Glances system monitoring module
Add secubox-app-glances and luci-app-glances packages: secubox-app-glances: - LXC container with nicolargo/glances:latest-full Docker image - Web UI on port 61208, API on port 61209 - UCI configuration for monitoring options and alert thresholds - glancesctl management script luci-app-glances: - Dashboard view with service status and quick actions - Embedded Web UI view with iframe - Settings view for configuration - RPCD backend with proper ACL permissions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
99aa610879
commit
4004f2bfe8
63
package/secubox/luci-app-glances/Makefile
Normal file
63
package/secubox/luci-app-glances/Makefile
Normal file
@ -0,0 +1,63 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2025-2026 CyberMind.fr
|
||||
#
|
||||
# LuCI Glances Dashboard - System Monitoring Interface
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-glances
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_ARCH:=all
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
LUCI_TITLE:=LuCI Glances Dashboard
|
||||
LUCI_DESCRIPTION:=Modern dashboard for Glances system monitoring with SecuBox theme
|
||||
LUCI_DEPENDS:=+luci-base +luci-app-secubox +secubox-app-glances
|
||||
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/glances
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
# RPCD backend (MUST be 755 for ubus calls)
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.glances $(1)/usr/libexec/rpcd/
|
||||
|
||||
# ACL permissions
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
|
||||
|
||||
# LuCI menu
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
|
||||
|
||||
# JavaScript resources
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/glances
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/glances/*.js $(1)/www/luci-static/resources/glances/ 2>/dev/null || true
|
||||
|
||||
# JavaScript views
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/glances
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/glances/*.js $(1)/www/luci-static/resources/view/glances/
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
# Restart RPCD to register new methods
|
||||
/etc/init.d/rpcd restart
|
||||
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
||||
echo "Glances Dashboard installed."
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot
|
||||
$(eval $(call BuildPackage,luci-app-glances))
|
||||
@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'get_status'
|
||||
});
|
||||
|
||||
var callGetConfig = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'get_config'
|
||||
});
|
||||
|
||||
var callGetMonitoringConfig = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'get_monitoring_config'
|
||||
});
|
||||
|
||||
var callGetAlertsConfig = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'get_alerts_config'
|
||||
});
|
||||
|
||||
var callGetWebUrl = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'get_web_url'
|
||||
});
|
||||
|
||||
var callServiceStart = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'service_start'
|
||||
});
|
||||
|
||||
var callServiceStop = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'service_stop'
|
||||
});
|
||||
|
||||
var callServiceRestart = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'service_restart'
|
||||
});
|
||||
|
||||
var callSetConfig = rpc.declare({
|
||||
object: 'luci.glances',
|
||||
method: 'set_config',
|
||||
params: ['key', 'value']
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: function() {
|
||||
return callGetStatus().catch(function() {
|
||||
return { running: false, enabled: false };
|
||||
});
|
||||
},
|
||||
|
||||
getConfig: function() {
|
||||
return callGetConfig().catch(function() {
|
||||
return {};
|
||||
});
|
||||
},
|
||||
|
||||
getMonitoringConfig: function() {
|
||||
return callGetMonitoringConfig().catch(function() {
|
||||
return {};
|
||||
});
|
||||
},
|
||||
|
||||
getAlertsConfig: function() {
|
||||
return callGetAlertsConfig().catch(function() {
|
||||
return {};
|
||||
});
|
||||
},
|
||||
|
||||
getWebUrl: function() {
|
||||
return callGetWebUrl().catch(function() {
|
||||
return { web_url: '' };
|
||||
});
|
||||
},
|
||||
|
||||
serviceStart: function() {
|
||||
return callServiceStart();
|
||||
},
|
||||
|
||||
serviceStop: function() {
|
||||
return callServiceStop();
|
||||
},
|
||||
|
||||
serviceRestart: function() {
|
||||
return callServiceRestart();
|
||||
},
|
||||
|
||||
setConfig: function(key, value) {
|
||||
return callSetConfig(key, value);
|
||||
},
|
||||
|
||||
getAllData: function() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
self.getStatus(),
|
||||
self.getConfig(),
|
||||
self.getMonitoringConfig(),
|
||||
self.getAlertsConfig()
|
||||
]).then(function(results) {
|
||||
return {
|
||||
status: results[0],
|
||||
config: results[1],
|
||||
monitoring: results[2],
|
||||
alerts: results[3]
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,198 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require glances.api as api';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||
Theme.init({ language: lang });
|
||||
|
||||
var GLANCES_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'webui', icon: '🖥️', label: 'Web UI' },
|
||||
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||
];
|
||||
|
||||
function renderGlancesNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'gl-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||
}, GLANCES_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'monitoring', 'glances', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#27ae60,#1e8449);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
function renderCard(title, icon, content, color) {
|
||||
return E('div', {
|
||||
'style': 'background:#1a1a1f;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:20px;'
|
||||
}, [
|
||||
E('div', { 'style': 'display:flex;align-items:center;gap:10px;margin-bottom:15px;' }, [
|
||||
E('span', { 'style': 'font-size:24px;' }, icon),
|
||||
E('h3', { 'style': 'margin:0;color:#fff;font-size:16px;' }, title)
|
||||
]),
|
||||
E('div', { 'style': 'color:' + (color || '#a0a0b0') + ';' }, content)
|
||||
]);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
title: _('Glances Dashboard'),
|
||||
|
||||
load: function() {
|
||||
return api.getAllData();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data.status || {};
|
||||
var config = data.config || {};
|
||||
var monitoring = data.monitoring || {};
|
||||
|
||||
var statusColor = status.running ? '#27ae60' : '#e74c3c';
|
||||
var statusText = status.running ? _('Running') : _('Stopped');
|
||||
|
||||
var content = E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:20px;' }, [
|
||||
// Status Card
|
||||
renderCard(_('Service Status'), '📡', [
|
||||
E('div', { 'style': 'display:flex;align-items:center;gap:10px;margin-bottom:15px;' }, [
|
||||
E('span', { 'style': 'width:12px;height:12px;border-radius:50%;background:' + statusColor + ';' }),
|
||||
E('span', { 'style': 'font-size:18px;font-weight:600;color:' + statusColor + ';' }, statusText)
|
||||
]),
|
||||
E('div', { 'style': 'display:grid;gap:8px;font-size:14px;' }, [
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('LXC State') + ': '),
|
||||
E('span', { 'style': 'color:#fff;' }, status.lxc_state || 'N/A')
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('PID') + ': '),
|
||||
E('span', { 'style': 'color:#fff;' }, status.pid || 'N/A')
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Web Port') + ': '),
|
||||
E('span', { 'style': 'color:#fff;' }, config.web_port || '61208')
|
||||
])
|
||||
]),
|
||||
E('div', { 'style': 'display:flex;gap:10px;margin-top:20px;' }, [
|
||||
status.running ?
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-remove',
|
||||
'style': 'flex:1;',
|
||||
'click': function() {
|
||||
ui.showModal(_('Stopping...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Stopping Glances...'))
|
||||
]);
|
||||
api.serviceStop().then(function() {
|
||||
ui.hideModal();
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}, _('Stop')) :
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'style': 'flex:1;',
|
||||
'click': function() {
|
||||
ui.showModal(_('Starting...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Starting Glances...'))
|
||||
]);
|
||||
api.serviceStart().then(function() {
|
||||
ui.hideModal();
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
});
|
||||
}
|
||||
}, _('Start')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'style': 'flex:1;',
|
||||
'click': function() {
|
||||
ui.showModal(_('Restarting...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Restarting Glances...'))
|
||||
]);
|
||||
api.serviceRestart().then(function() {
|
||||
ui.hideModal();
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
});
|
||||
}
|
||||
}, _('Restart'))
|
||||
])
|
||||
]),
|
||||
|
||||
// Configuration Card
|
||||
renderCard(_('Configuration'), '⚙️', [
|
||||
E('div', { 'style': 'display:grid;gap:8px;font-size:14px;' }, [
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Enabled') + ': '),
|
||||
E('span', { 'style': 'color:' + (config.enabled ? '#27ae60' : '#e74c3c') + ';' },
|
||||
config.enabled ? _('Yes') : _('No'))
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Refresh Rate') + ': '),
|
||||
E('span', { 'style': 'color:#fff;' }, (config.refresh_rate || 3) + 's')
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Memory Limit') + ': '),
|
||||
E('span', { 'style': 'color:#fff;' }, config.memory_limit || '128M')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Monitoring Card
|
||||
renderCard(_('Monitoring'), '📈', [
|
||||
E('div', { 'style': 'display:grid;gap:8px;font-size:14px;' }, [
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Docker') + ': '),
|
||||
E('span', { 'style': 'color:' + (monitoring.monitor_docker ? '#27ae60' : '#666') + ';' },
|
||||
monitoring.monitor_docker ? '✓' : '✗')
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Network') + ': '),
|
||||
E('span', { 'style': 'color:' + (monitoring.monitor_network ? '#27ae60' : '#666') + ';' },
|
||||
monitoring.monitor_network ? '✓' : '✗')
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Disk I/O') + ': '),
|
||||
E('span', { 'style': 'color:' + (monitoring.monitor_diskio ? '#27ae60' : '#666') + ';' },
|
||||
monitoring.monitor_diskio ? '✓' : '✗')
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'style': 'color:#666;' }, _('Sensors') + ': '),
|
||||
E('span', { 'style': 'color:' + (monitoring.monitor_sensors ? '#27ae60' : '#666') + ';' },
|
||||
monitoring.monitor_sensors ? '✓' : '✗')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Quick Access Card
|
||||
renderCard(_('Quick Access'), '🔗', [
|
||||
status.running ? [
|
||||
E('a', {
|
||||
'href': status.web_url,
|
||||
'target': '_blank',
|
||||
'style': 'display:block;padding:12px;background:#27ae60;color:white;text-align:center;border-radius:8px;text-decoration:none;font-weight:500;'
|
||||
}, '🖥️ ' + _('Open Glances Web UI')),
|
||||
E('p', { 'style': 'margin-top:10px;font-size:13px;color:#666;' },
|
||||
_('URL') + ': ' + status.web_url)
|
||||
] : E('p', { 'style': 'color:#666;' }, _('Start Glances to access the Web UI'))
|
||||
])
|
||||
]);
|
||||
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(renderGlancesNav('dashboard'));
|
||||
wrapper.appendChild(content);
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require glances.api as api';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||
Theme.init({ language: lang });
|
||||
|
||||
var GLANCES_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'webui', icon: '🖥️', label: 'Web UI' },
|
||||
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||
];
|
||||
|
||||
function renderGlancesNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'gl-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||
}, GLANCES_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'monitoring', 'glances', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#27ae60,#1e8449);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
title: _('Glances Settings'),
|
||||
|
||||
load: function() {
|
||||
return uci.load('glances');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('glances', _('Glances Settings'),
|
||||
_('Configure Glances system monitoring service.'));
|
||||
|
||||
// Main Settings Section
|
||||
s = m.section(form.TypedSection, 'glances', _('Main Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.rmempty = false;
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Value, 'web_port', _('Web Port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '61208';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'api_port', _('API Port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '61209';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'web_host', _('Listen Address'));
|
||||
o.default = '0.0.0.0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'refresh_rate', _('Refresh Rate (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'memory_limit', _('Memory Limit'));
|
||||
o.default = '128M';
|
||||
o.rmempty = false;
|
||||
o.description = _('Container memory limit (e.g., 128M, 256M)');
|
||||
|
||||
// Monitoring Section
|
||||
s = m.section(form.TypedSection, 'monitoring', _('Monitoring Options'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'monitor_docker', _('Monitor Docker'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'monitor_network', _('Monitor Network'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'monitor_diskio', _('Monitor Disk I/O'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'monitor_sensors', _('Monitor Sensors'));
|
||||
o.default = '1';
|
||||
|
||||
o = s.option(form.Flag, 'monitor_processes', _('Monitor Processes'));
|
||||
o.default = '1';
|
||||
|
||||
// Alerts Section
|
||||
s = m.section(form.TypedSection, 'alerts', _('Alert Thresholds'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Value, 'cpu_warning', _('CPU Warning (%)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.default = '70';
|
||||
|
||||
o = s.option(form.Value, 'cpu_critical', _('CPU Critical (%)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.default = '90';
|
||||
|
||||
o = s.option(form.Value, 'mem_warning', _('Memory Warning (%)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.default = '70';
|
||||
|
||||
o = s.option(form.Value, 'mem_critical', _('Memory Critical (%)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.default = '90';
|
||||
|
||||
o = s.option(form.Value, 'disk_warning', _('Disk Warning (%)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.default = '70';
|
||||
|
||||
o = s.option(form.Value, 'disk_critical', _('Disk Critical (%)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.default = '90';
|
||||
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(renderGlancesNav('settings'));
|
||||
|
||||
return m.render().then(function(formEl) {
|
||||
wrapper.appendChild(formEl);
|
||||
return wrapper;
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require glances.api as api';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require secubox-portal/header as SbHeader';
|
||||
|
||||
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||
Theme.init({ language: lang });
|
||||
|
||||
var GLANCES_NAV = [
|
||||
{ id: 'dashboard', icon: '📊', label: 'Dashboard' },
|
||||
{ id: 'webui', icon: '🖥️', label: 'Web UI' },
|
||||
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||
];
|
||||
|
||||
function renderGlancesNav(activeId) {
|
||||
return E('div', {
|
||||
'class': 'gl-app-nav',
|
||||
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;'
|
||||
}, GLANCES_NAV.map(function(item) {
|
||||
var isActive = activeId === item.id;
|
||||
return E('a', {
|
||||
'href': L.url('admin', 'secubox', 'monitoring', 'glances', item.id),
|
||||
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||
(isActive ? 'background:linear-gradient(135deg,#27ae60,#1e8449);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||
}, [
|
||||
E('span', {}, item.icon),
|
||||
E('span', {}, _(item.label))
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
title: _('Glances Web UI'),
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
api.getStatus(),
|
||||
api.getWebUrl()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var urlData = data[1] || {};
|
||||
|
||||
var content;
|
||||
|
||||
if (!status.running) {
|
||||
content = E('div', { 'class': 'gl-card', 'style': 'text-align: center; padding: 60px 20px; background: #1a1a1f; border-radius: 12px;' }, [
|
||||
E('div', { 'style': 'font-size: 64px; margin-bottom: 20px;' }, '⚠️'),
|
||||
E('h2', { 'style': 'margin: 0 0 10px 0; color: #f39c12;' }, _('Glances is not running')),
|
||||
E('p', { 'style': 'color: #a0a0b0; margin: 0 0 20px 0;' }, _('Start the service to access the Web UI')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() {
|
||||
ui.showModal(_('Starting...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Starting Glances...'))
|
||||
]);
|
||||
api.serviceStart().then(function() {
|
||||
ui.hideModal();
|
||||
setTimeout(function() { location.reload(); }, 3000);
|
||||
});
|
||||
}
|
||||
}, '▶ ' + _('Start Glances'))
|
||||
]);
|
||||
} else {
|
||||
var iframeSrc = urlData.web_url;
|
||||
|
||||
content = E('div', { 'style': 'display: flex; flex-direction: column; height: calc(100vh - 200px); min-height: 600px;' }, [
|
||||
// Toolbar
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: 12px; margin-bottom: 12px; padding: 12px 16px; background: #141419; border-radius: 8px;' }, [
|
||||
E('span', { 'style': 'color: #27ae60; font-weight: 500;' }, '● ' + _('Connected')),
|
||||
E('span', { 'style': 'color: #a0a0b0; font-size: 13px;' }, iframeSrc),
|
||||
E('div', { 'style': 'flex: 1;' }),
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': function() {
|
||||
var iframe = document.querySelector('.glances-iframe');
|
||||
if (iframe) iframe.src = iframe.src;
|
||||
}
|
||||
}, '🔄 ' + _('Refresh')),
|
||||
E('a', {
|
||||
'class': 'btn',
|
||||
'href': iframeSrc,
|
||||
'target': '_blank',
|
||||
'style': 'text-decoration: none;'
|
||||
}, '↗ ' + _('Open in New Tab'))
|
||||
]),
|
||||
|
||||
// Iframe container
|
||||
E('div', {
|
||||
'style': 'flex: 1; border-radius: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);'
|
||||
}, [
|
||||
E('iframe', {
|
||||
'class': 'glances-iframe',
|
||||
'src': iframeSrc,
|
||||
'style': 'width: 100%; height: 100%; border: none; background: #1a1a1f;',
|
||||
'allow': 'fullscreen'
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||
wrapper.appendChild(SbHeader.render());
|
||||
wrapper.appendChild(renderGlancesNav('webui'));
|
||||
wrapper.appendChild(content);
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,220 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# RPCD backend for Glances LuCI interface
|
||||
# Copyright (C) 2025-2026 CyberMind.fr (SecuBox)
|
||||
#
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
LXC_NAME="glances"
|
||||
|
||||
# Get service status
|
||||
get_status() {
|
||||
local running=0
|
||||
local pid=""
|
||||
local lxc_state=""
|
||||
local web_url=""
|
||||
|
||||
# Check LXC container status
|
||||
if command -v lxc-info >/dev/null 2>&1; then
|
||||
lxc_state=$(lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -oE 'RUNNING|STOPPED' || echo "UNKNOWN")
|
||||
if [ "$lxc_state" = "RUNNING" ]; then
|
||||
running=1
|
||||
pid=$(lxc-info -n "$LXC_NAME" -p 2>/dev/null | grep -oE '[0-9]+' || echo "0")
|
||||
fi
|
||||
fi
|
||||
|
||||
local enabled=$(uci -q get glances.main.enabled || echo "0")
|
||||
local web_port=$(uci -q get glances.main.web_port || echo "61208")
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
|
||||
[ "$running" = "1" ] && web_url="http://${router_ip}:${web_port}"
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"pid": ${pid:-0},
|
||||
"lxc_state": "$lxc_state",
|
||||
"web_port": $web_port,
|
||||
"web_url": "$web_url"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get main configuration
|
||||
get_config() {
|
||||
local enabled=$(uci -q get glances.main.enabled || echo "0")
|
||||
local web_port=$(uci -q get glances.main.web_port || echo "61208")
|
||||
local api_port=$(uci -q get glances.main.api_port || echo "61209")
|
||||
local web_host=$(uci -q get glances.main.web_host || echo "0.0.0.0")
|
||||
local refresh_rate=$(uci -q get glances.main.refresh_rate || echo "3")
|
||||
local memory_limit=$(uci -q get glances.main.memory_limit || echo "128M")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
||||
"web_port": $web_port,
|
||||
"api_port": $api_port,
|
||||
"web_host": "$web_host",
|
||||
"refresh_rate": $refresh_rate,
|
||||
"memory_limit": "$memory_limit"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get monitoring configuration
|
||||
get_monitoring_config() {
|
||||
local monitor_docker=$(uci -q get glances.monitoring.monitor_docker || echo "1")
|
||||
local monitor_network=$(uci -q get glances.monitoring.monitor_network || echo "1")
|
||||
local monitor_diskio=$(uci -q get glances.monitoring.monitor_diskio || echo "1")
|
||||
local monitor_sensors=$(uci -q get glances.monitoring.monitor_sensors || echo "1")
|
||||
local monitor_processes=$(uci -q get glances.monitoring.monitor_processes || echo "1")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"monitor_docker": $([ "$monitor_docker" = "1" ] && echo "true" || echo "false"),
|
||||
"monitor_network": $([ "$monitor_network" = "1" ] && echo "true" || echo "false"),
|
||||
"monitor_diskio": $([ "$monitor_diskio" = "1" ] && echo "true" || echo "false"),
|
||||
"monitor_sensors": $([ "$monitor_sensors" = "1" ] && echo "true" || echo "false"),
|
||||
"monitor_processes": $([ "$monitor_processes" = "1" ] && echo "true" || echo "false")
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get alerts configuration
|
||||
get_alerts_config() {
|
||||
local cpu_warning=$(uci -q get glances.alerts.cpu_warning || echo "70")
|
||||
local cpu_critical=$(uci -q get glances.alerts.cpu_critical || echo "90")
|
||||
local mem_warning=$(uci -q get glances.alerts.mem_warning || echo "70")
|
||||
local mem_critical=$(uci -q get glances.alerts.mem_critical || echo "90")
|
||||
local disk_warning=$(uci -q get glances.alerts.disk_warning || echo "70")
|
||||
local disk_critical=$(uci -q get glances.alerts.disk_critical || echo "90")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"cpu_warning": $cpu_warning,
|
||||
"cpu_critical": $cpu_critical,
|
||||
"mem_warning": $mem_warning,
|
||||
"mem_critical": $mem_critical,
|
||||
"disk_warning": $disk_warning,
|
||||
"disk_critical": $disk_critical
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Get web URL for iframe
|
||||
get_web_url() {
|
||||
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
||||
local web_port=$(uci -q get glances.main.web_port || echo "61208")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"web_url": "http://$router_ip:$web_port"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Service control
|
||||
service_start() {
|
||||
/etc/init.d/glances start >/dev/null 2>&1
|
||||
sleep 2
|
||||
get_status
|
||||
}
|
||||
|
||||
service_stop() {
|
||||
/etc/init.d/glances stop >/dev/null 2>&1
|
||||
sleep 1
|
||||
get_status
|
||||
}
|
||||
|
||||
service_restart() {
|
||||
/etc/init.d/glances restart >/dev/null 2>&1
|
||||
sleep 2
|
||||
get_status
|
||||
}
|
||||
|
||||
# Set configuration
|
||||
set_config() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local section="main"
|
||||
|
||||
case "$key" in
|
||||
monitor_*)
|
||||
section="monitoring"
|
||||
;;
|
||||
cpu_*|mem_*|disk_*)
|
||||
section="alerts"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle boolean conversion
|
||||
case "$value" in
|
||||
true) value="1" ;;
|
||||
false) value="0" ;;
|
||||
esac
|
||||
|
||||
uci set "glances.$section.$key=$value"
|
||||
uci commit glances
|
||||
echo '{"success":true}'
|
||||
}
|
||||
|
||||
# RPCD list method
|
||||
case "$1" in
|
||||
list)
|
||||
cat <<EOF
|
||||
{
|
||||
"get_status": {},
|
||||
"get_config": {},
|
||||
"get_monitoring_config": {},
|
||||
"get_alerts_config": {},
|
||||
"get_web_url": {},
|
||||
"service_start": {},
|
||||
"service_stop": {},
|
||||
"service_restart": {},
|
||||
"set_config": {"key": "string", "value": "string"}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
get_status)
|
||||
get_status
|
||||
;;
|
||||
get_config)
|
||||
get_config
|
||||
;;
|
||||
get_monitoring_config)
|
||||
get_monitoring_config
|
||||
;;
|
||||
get_alerts_config)
|
||||
get_alerts_config
|
||||
;;
|
||||
get_web_url)
|
||||
get_web_url
|
||||
;;
|
||||
service_start)
|
||||
service_start
|
||||
;;
|
||||
service_stop)
|
||||
service_stop
|
||||
;;
|
||||
service_restart)
|
||||
service_restart
|
||||
;;
|
||||
set_config)
|
||||
read -r input
|
||||
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||
set_config "$key" "$value"
|
||||
;;
|
||||
*)
|
||||
echo '{"error":"Unknown method"}'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo '{"error":"Unknown command"}'
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,38 @@
|
||||
{
|
||||
"admin/secubox/monitoring/glances": {
|
||||
"title": "Glances",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "firstchild",
|
||||
"recurse": true
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-glances"],
|
||||
"uci": {"glances": true}
|
||||
}
|
||||
},
|
||||
"admin/secubox/monitoring/glances/dashboard": {
|
||||
"title": "Dashboard",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "glances/dashboard"
|
||||
}
|
||||
},
|
||||
"admin/secubox/monitoring/glances/webui": {
|
||||
"title": "Web UI",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "glances/webui"
|
||||
}
|
||||
},
|
||||
"admin/secubox/monitoring/glances/settings": {
|
||||
"title": "Settings",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "glances/settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
{
|
||||
"luci-app-glances": {
|
||||
"description": "Grant access to Glances LuCI app",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.glances": [
|
||||
"get_status",
|
||||
"get_config",
|
||||
"get_monitoring_config",
|
||||
"get_alerts_config",
|
||||
"get_web_url"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"glances"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.glances": [
|
||||
"service_start",
|
||||
"service_stop",
|
||||
"service_restart",
|
||||
"set_config"
|
||||
]
|
||||
},
|
||||
"uci": [
|
||||
"glances"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
70
package/secubox/secubox-app-glances/Makefile
Normal file
70
package/secubox/secubox-app-glances/Makefile
Normal file
@ -0,0 +1,70 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-glances
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_ARCH:=all
|
||||
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||
PKG_LICENSE:=LGPL-3.0
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/secubox-app-glances
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
PKGARCH:=all
|
||||
SUBMENU:=SecuBox Apps
|
||||
TITLE:=SecuBox Glances System Monitor (LXC)
|
||||
DEPENDS:=+uci +libuci +wget +tar
|
||||
endef
|
||||
|
||||
define Package/secubox-app-glances/description
|
||||
Glances - Cross-platform system monitoring tool for SecuBox.
|
||||
|
||||
Features:
|
||||
- Real-time CPU, memory, disk, network monitoring
|
||||
- Process list with resource usage
|
||||
- Docker/Podman container monitoring
|
||||
- Web-based UI accessible from any device
|
||||
- RESTful JSON API for integrations
|
||||
- Alert system for thresholds
|
||||
|
||||
Runs in LXC container for isolation and security.
|
||||
Configure in /etc/config/glances.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-glances/conffiles
|
||||
/etc/config/glances
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/secubox-app-glances/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/etc/config/glances $(1)/etc/config/glances
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/etc/init.d/glances $(1)/etc/init.d/glances
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/glancesctl $(1)/usr/sbin/glancesctl
|
||||
endef
|
||||
|
||||
define Package/secubox-app-glances/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
echo ""
|
||||
echo "Glances installed."
|
||||
echo ""
|
||||
echo "To install and start Glances:"
|
||||
echo " glancesctl install"
|
||||
echo " /etc/init.d/glances start"
|
||||
echo ""
|
||||
echo "Web interface: http://<router-ip>:61208"
|
||||
echo ""
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-glances))
|
||||
23
package/secubox/secubox-app-glances/files/etc/config/glances
Normal file
23
package/secubox/secubox-app-glances/files/etc/config/glances
Normal file
@ -0,0 +1,23 @@
|
||||
config glances 'main'
|
||||
option enabled '0'
|
||||
option runtime 'lxc'
|
||||
option web_port '61208'
|
||||
option api_port '61209'
|
||||
option web_host '0.0.0.0'
|
||||
option refresh_rate '3'
|
||||
option memory_limit '128M'
|
||||
|
||||
config monitoring 'monitoring'
|
||||
option monitor_docker '1'
|
||||
option monitor_network '1'
|
||||
option monitor_diskio '1'
|
||||
option monitor_sensors '1'
|
||||
option monitor_processes '1'
|
||||
|
||||
config alerts 'alerts'
|
||||
option cpu_warning '70'
|
||||
option cpu_critical '90'
|
||||
option mem_warning '70'
|
||||
option mem_critical '90'
|
||||
option disk_warning '70'
|
||||
option disk_critical '90'
|
||||
42
package/secubox/secubox-app-glances/files/etc/init.d/glances
Normal file
42
package/secubox/secubox-app-glances/files/etc/init.d/glances
Normal file
@ -0,0 +1,42 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Glances system monitor init script
|
||||
# Copyright (C) 2025-2026 CyberMind.fr
|
||||
|
||||
START=95
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/sbin/glancesctl
|
||||
CONFIG=glances
|
||||
|
||||
start_service() {
|
||||
local enabled
|
||||
config_load "$CONFIG"
|
||||
config_get enabled main enabled '0'
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
procd_open_instance glances
|
||||
procd_set_param command "$PROG" service-run
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
"$PROG" service-stop
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIG"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
status() {
|
||||
"$PROG" status
|
||||
}
|
||||
409
package/secubox/secubox-app-glances/files/usr/sbin/glancesctl
Normal file
409
package/secubox/secubox-app-glances/files/usr/sbin/glancesctl
Normal file
@ -0,0 +1,409 @@
|
||||
#!/bin/sh
|
||||
# SecuBox Glances manager - LXC container support
|
||||
# Copyright (C) 2025-2026 CyberMind.fr
|
||||
|
||||
CONFIG="glances"
|
||||
LXC_NAME="glances"
|
||||
OPKG_UPDATED=0
|
||||
|
||||
# Paths
|
||||
LXC_PATH="/srv/lxc"
|
||||
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
||||
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: glancesctl <command>
|
||||
|
||||
Commands:
|
||||
install Install prerequisites and create LXC container
|
||||
check Run prerequisite checks
|
||||
update Update Glances in container
|
||||
status Show container status
|
||||
logs Show Glances logs (use -f to follow)
|
||||
shell Open shell in container
|
||||
service-run Internal: run container under procd
|
||||
service-stop Stop container
|
||||
|
||||
Web Interface: http://<router-ip>:61208
|
||||
API Endpoint: http://<router-ip>:61209
|
||||
EOF
|
||||
}
|
||||
|
||||
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
|
||||
|
||||
log_info() { echo "[INFO] $*"; }
|
||||
log_warn() { echo "[WARN] $*" >&2; }
|
||||
log_error() { echo "[ERROR] $*" >&2; }
|
||||
|
||||
uci_get() { uci -q get ${CONFIG}.$1; }
|
||||
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
||||
|
||||
# Load configuration with defaults
|
||||
load_config() {
|
||||
web_port="$(uci_get main.web_port || echo 61208)"
|
||||
api_port="$(uci_get main.api_port || echo 61209)"
|
||||
web_host="$(uci_get main.web_host || echo 0.0.0.0)"
|
||||
refresh_rate="$(uci_get main.refresh_rate || echo 3)"
|
||||
memory_limit="$(uci_get main.memory_limit || echo 128M)"
|
||||
|
||||
# Monitoring options
|
||||
monitor_docker="$(uci_get monitoring.monitor_docker || echo 1)"
|
||||
monitor_network="$(uci_get monitoring.monitor_network || echo 1)"
|
||||
monitor_diskio="$(uci_get monitoring.monitor_diskio || echo 1)"
|
||||
monitor_sensors="$(uci_get monitoring.monitor_sensors || echo 1)"
|
||||
|
||||
# Alert thresholds
|
||||
cpu_warning="$(uci_get alerts.cpu_warning || echo 70)"
|
||||
cpu_critical="$(uci_get alerts.cpu_critical || echo 90)"
|
||||
mem_warning="$(uci_get alerts.mem_warning || echo 70)"
|
||||
mem_critical="$(uci_get alerts.mem_critical || echo 90)"
|
||||
}
|
||||
|
||||
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
||||
|
||||
has_lxc() {
|
||||
command -v lxc-start >/dev/null 2>&1 && \
|
||||
command -v lxc-stop >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Ensure required packages are installed
|
||||
ensure_packages() {
|
||||
require_root
|
||||
for pkg in "$@"; do
|
||||
if ! opkg list-installed | grep -q "^$pkg "; then
|
||||
if [ "$OPKG_UPDATED" -eq 0 ]; then
|
||||
opkg update || return 1
|
||||
OPKG_UPDATED=1
|
||||
fi
|
||||
opkg install "$pkg" || return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# LXC CONTAINER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
lxc_check_prereqs() {
|
||||
log_info "Checking LXC prerequisites..."
|
||||
ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1
|
||||
|
||||
if [ ! -d /sys/fs/cgroup ]; then
|
||||
log_error "cgroups not mounted at /sys/fs/cgroup"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "LXC ready"
|
||||
}
|
||||
|
||||
lxc_create_rootfs() {
|
||||
load_config
|
||||
|
||||
if [ -d "$LXC_ROOTFS" ] && [ -x "$LXC_ROOTFS/usr/local/bin/glances" ]; then
|
||||
log_info "LXC rootfs already exists with Glances"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating LXC rootfs for Glances..."
|
||||
ensure_dir "$LXC_PATH/$LXC_NAME"
|
||||
|
||||
lxc_create_docker_rootfs || return 1
|
||||
lxc_create_config || return 1
|
||||
|
||||
log_info "LXC rootfs created successfully"
|
||||
}
|
||||
|
||||
lxc_create_docker_rootfs() {
|
||||
local rootfs="$LXC_ROOTFS"
|
||||
local image="nicolargo/glances"
|
||||
local tag="latest-full"
|
||||
local registry="registry-1.docker.io"
|
||||
local arch
|
||||
|
||||
# Detect architecture for Docker manifest
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch="amd64" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
armv7l) arch="arm" ;;
|
||||
*) arch="amd64" ;;
|
||||
esac
|
||||
|
||||
log_info "Extracting Glances Docker image ($arch)..."
|
||||
ensure_dir "$rootfs"
|
||||
|
||||
# Get Docker Hub token
|
||||
local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jsonfilter -e '@.token')
|
||||
[ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; }
|
||||
|
||||
# Get manifest list
|
||||
local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
|
||||
--header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
||||
"https://$registry/v2/$image/manifests/$tag")
|
||||
|
||||
# Find digest for our architecture
|
||||
local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest")
|
||||
[ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; }
|
||||
|
||||
# Get image manifest
|
||||
local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \
|
||||
--header="Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
||||
"https://$registry/v2/$image/manifests/$digest")
|
||||
|
||||
# Extract layers and download them
|
||||
log_info "Downloading and extracting layers..."
|
||||
local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest')
|
||||
|
||||
for layer_digest in $layers; do
|
||||
log_info " Layer: ${layer_digest:7:12}..."
|
||||
wget -q -O - --header="Authorization: Bearer $token" \
|
||||
"https://$registry/v2/$image/blobs/$layer_digest" | \
|
||||
tar xzf - -C "$rootfs" 2>&1 | grep -v "Cannot change ownership" || true
|
||||
done
|
||||
|
||||
# Configure container
|
||||
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
|
||||
mkdir -p "$rootfs/var/log/glances" "$rootfs/etc/glances" "$rootfs/tmp"
|
||||
|
||||
# Ensure /bin/sh exists
|
||||
if [ ! -x "$rootfs/bin/sh" ]; then
|
||||
if [ -x "$rootfs/bin/dash" ]; then
|
||||
ln -sf dash "$rootfs/bin/sh"
|
||||
elif [ -x "$rootfs/bin/bash" ]; then
|
||||
ln -sf bash "$rootfs/bin/sh"
|
||||
elif [ -x "$rootfs/usr/bin/dash" ]; then
|
||||
mkdir -p "$rootfs/bin"
|
||||
ln -sf /usr/bin/dash "$rootfs/bin/sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create startup script
|
||||
cat > "$rootfs/opt/start-glances.sh" << 'START'
|
||||
#!/bin/sh
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
|
||||
cd /
|
||||
|
||||
# Read environment variables
|
||||
WEB_PORT="${GLANCES_WEB_PORT:-61208}"
|
||||
WEB_HOST="${GLANCES_WEB_HOST:-0.0.0.0}"
|
||||
REFRESH="${GLANCES_REFRESH:-3}"
|
||||
|
||||
# Build args for web server mode
|
||||
ARGS="-w --bind $WEB_HOST:$WEB_PORT -t $REFRESH"
|
||||
|
||||
# Disable plugins if configured
|
||||
[ "$GLANCES_NO_DOCKER" = "1" ] && ARGS="$ARGS --disable-plugin docker"
|
||||
[ "$GLANCES_NO_SENSORS" = "1" ] && ARGS="$ARGS --disable-plugin sensors"
|
||||
|
||||
echo "Starting Glances web server on $WEB_HOST:$WEB_PORT..."
|
||||
exec /usr/local/bin/glances $ARGS
|
||||
START
|
||||
chmod +x "$rootfs/opt/start-glances.sh"
|
||||
|
||||
log_info "Glances Docker image extracted successfully"
|
||||
}
|
||||
|
||||
lxc_create_config() {
|
||||
load_config
|
||||
|
||||
cat > "$LXC_CONFIG" << EOF
|
||||
# Glances LXC Configuration
|
||||
lxc.uts.name = $LXC_NAME
|
||||
|
||||
# Root filesystem
|
||||
lxc.rootfs.path = dir:$LXC_ROOTFS
|
||||
|
||||
# Network - use host network for full system visibility
|
||||
lxc.net.0.type = none
|
||||
|
||||
# Mounts - give access to host system info
|
||||
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
||||
lxc.mount.entry = /proc/stat proc/stat none bind,ro 0 0
|
||||
lxc.mount.entry = /proc/meminfo proc/meminfo none bind,ro 0 0
|
||||
lxc.mount.entry = /proc/cpuinfo proc/cpuinfo none bind,ro 0 0
|
||||
lxc.mount.entry = /proc/uptime proc/uptime none bind,ro 0 0
|
||||
lxc.mount.entry = /proc/loadavg proc/loadavg none bind,ro 0 0
|
||||
lxc.mount.entry = /proc/net proc/net none bind,ro 0 0
|
||||
lxc.mount.entry = /proc/diskstats proc/diskstats none bind,ro 0 0
|
||||
|
||||
# Environment variables
|
||||
lxc.environment = GLANCES_WEB_PORT=$web_port
|
||||
lxc.environment = GLANCES_WEB_HOST=$web_host
|
||||
lxc.environment = GLANCES_REFRESH=$refresh_rate
|
||||
lxc.environment = GLANCES_NO_DOCKER=$([ "$monitor_docker" = "0" ] && echo 1 || echo 0)
|
||||
lxc.environment = GLANCES_NO_SENSORS=$([ "$monitor_sensors" = "0" ] && echo 1 || echo 0)
|
||||
|
||||
# Capabilities - minimal for monitoring
|
||||
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_rawio
|
||||
|
||||
# cgroups limits
|
||||
lxc.cgroup.memory.limit_in_bytes = $memory_limit
|
||||
|
||||
# Init
|
||||
lxc.init.cmd = /opt/start-glances.sh
|
||||
|
||||
# Console
|
||||
lxc.console.size = 1024
|
||||
lxc.pty.max = 1024
|
||||
EOF
|
||||
|
||||
log_info "LXC config created at $LXC_CONFIG"
|
||||
}
|
||||
|
||||
lxc_stop() {
|
||||
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
|
||||
lxc-stop -n "$LXC_NAME" -k >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
lxc_run() {
|
||||
load_config
|
||||
lxc_stop
|
||||
|
||||
if [ ! -f "$LXC_CONFIG" ]; then
|
||||
log_error "LXC not configured. Run 'glancesctl install' first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Regenerate config to pick up any UCI changes
|
||||
lxc_create_config
|
||||
|
||||
log_info "Starting Glances LXC container..."
|
||||
log_info "Web interface: http://0.0.0.0:$web_port"
|
||||
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
||||
}
|
||||
|
||||
lxc_status() {
|
||||
load_config
|
||||
echo "=== Glances Status ==="
|
||||
echo ""
|
||||
|
||||
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
|
||||
lxc-info -n "$LXC_NAME"
|
||||
else
|
||||
echo "LXC container '$LXC_NAME' not found or not configured"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Configuration ==="
|
||||
echo "Web port: $web_port"
|
||||
echo "Refresh rate: ${refresh_rate}s"
|
||||
echo "Memory limit: $memory_limit"
|
||||
}
|
||||
|
||||
lxc_logs() {
|
||||
if [ "$1" = "-f" ]; then
|
||||
logread -f -e glances
|
||||
else
|
||||
logread -e glances | tail -100
|
||||
fi
|
||||
}
|
||||
|
||||
lxc_shell() {
|
||||
lxc-attach -n "$LXC_NAME" -- /bin/sh
|
||||
}
|
||||
|
||||
lxc_destroy() {
|
||||
lxc_stop
|
||||
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
|
||||
rm -rf "$LXC_PATH/$LXC_NAME"
|
||||
log_info "LXC container destroyed"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# COMMANDS
|
||||
# =============================================================================
|
||||
|
||||
cmd_install() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
if ! has_lxc; then
|
||||
log_error "LXC not available. Install lxc packages first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Installing Glances..."
|
||||
|
||||
lxc_check_prereqs || exit 1
|
||||
lxc_create_rootfs || exit 1
|
||||
|
||||
uci_set main.enabled '1'
|
||||
/etc/init.d/glances enable
|
||||
|
||||
log_info "Glances installed."
|
||||
log_info "Start with: /etc/init.d/glances start"
|
||||
log_info "Web interface: http://<router-ip>:$web_port"
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
load_config
|
||||
|
||||
log_info "Checking prerequisites..."
|
||||
if has_lxc; then
|
||||
log_info "LXC: available"
|
||||
lxc_check_prereqs
|
||||
else
|
||||
log_warn "LXC: not available"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
log_info "Updating Glances..."
|
||||
lxc_destroy
|
||||
lxc_create_rootfs || exit 1
|
||||
|
||||
if /etc/init.d/glances enabled >/dev/null 2>&1; then
|
||||
/etc/init.d/glances restart
|
||||
else
|
||||
log_info "Update complete. Restart manually to apply."
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
lxc_status
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
lxc_logs "$@"
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
lxc_shell
|
||||
}
|
||||
|
||||
cmd_service_run() {
|
||||
require_root
|
||||
load_config
|
||||
|
||||
if ! has_lxc; then
|
||||
log_error "LXC not available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lxc_check_prereqs || exit 1
|
||||
lxc_run
|
||||
}
|
||||
|
||||
cmd_service_stop() {
|
||||
require_root
|
||||
lxc_stop
|
||||
}
|
||||
|
||||
# Main Entry Point
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
check) shift; cmd_check "$@" ;;
|
||||
update) shift; cmd_update "$@" ;;
|
||||
status) shift; cmd_status "$@" ;;
|
||||
logs) shift; cmd_logs "$@" ;;
|
||||
shell) shift; cmd_shell "$@" ;;
|
||||
service-run) shift; cmd_service_run "$@" ;;
|
||||
service-stop) shift; cmd_service_stop "$@" ;;
|
||||
help|--help|-h|'') usage ;;
|
||||
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
Loading…
Reference in New Issue
Block a user