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