diff --git a/package/secubox/luci-app-glances/Makefile b/package/secubox/luci-app-glances/Makefile new file mode 100644 index 00000000..5788ad5b --- /dev/null +++ b/package/secubox/luci-app-glances/Makefile @@ -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 + +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)) diff --git a/package/secubox/luci-app-glances/htdocs/luci-static/resources/glances/api.js b/package/secubox/luci-app-glances/htdocs/luci-static/resources/glances/api.js new file mode 100644 index 00000000..a7e9aca2 --- /dev/null +++ b/package/secubox/luci-app-glances/htdocs/luci-static/resources/glances/api.js @@ -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] + }; + }); + } +}); diff --git a/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/dashboard.js b/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/dashboard.js new file mode 100644 index 00000000..ecc5bbfa --- /dev/null +++ b/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/dashboard.js @@ -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 +}); diff --git a/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/settings.js b/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/settings.js new file mode 100644 index 00000000..259c5a57 --- /dev/null +++ b/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/settings.js @@ -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; + }); + } +}); diff --git a/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/webui.js b/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/webui.js new file mode 100644 index 00000000..f3df74a1 --- /dev/null +++ b/package/secubox/luci-app-glances/htdocs/luci-static/resources/view/glances/webui.js @@ -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 +}); diff --git a/package/secubox/luci-app-glances/root/usr/libexec/rpcd/luci.glances b/package/secubox/luci-app-glances/root/usr/libexec/rpcd/luci.glances new file mode 100644 index 00000000..57c1d875 --- /dev/null +++ b/package/secubox/luci-app-glances/root/usr/libexec/rpcd/luci.glances @@ -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 </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 </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 diff --git a/package/secubox/luci-app-glances/root/usr/share/luci/menu.d/luci-app-glances.json b/package/secubox/luci-app-glances/root/usr/share/luci/menu.d/luci-app-glances.json new file mode 100644 index 00000000..7695c906 --- /dev/null +++ b/package/secubox/luci-app-glances/root/usr/share/luci/menu.d/luci-app-glances.json @@ -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" + } + } +} diff --git a/package/secubox/luci-app-glances/root/usr/share/rpcd/acl.d/luci-app-glances.json b/package/secubox/luci-app-glances/root/usr/share/rpcd/acl.d/luci-app-glances.json new file mode 100644 index 00000000..cc7aed4a --- /dev/null +++ b/package/secubox/luci-app-glances/root/usr/share/rpcd/acl.d/luci-app-glances.json @@ -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" + ] + } + } +} diff --git a/package/secubox/secubox-app-glances/Makefile b/package/secubox/secubox-app-glances/Makefile new file mode 100644 index 00000000..203b1769 --- /dev/null +++ b/package/secubox/secubox-app-glances/Makefile @@ -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 +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://:61208" + echo "" +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-glances)) diff --git a/package/secubox/secubox-app-glances/files/etc/config/glances b/package/secubox/secubox-app-glances/files/etc/config/glances new file mode 100644 index 00000000..65a8d1ca --- /dev/null +++ b/package/secubox/secubox-app-glances/files/etc/config/glances @@ -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' diff --git a/package/secubox/secubox-app-glances/files/etc/init.d/glances b/package/secubox/secubox-app-glances/files/etc/init.d/glances new file mode 100644 index 00000000..a5a3c148 --- /dev/null +++ b/package/secubox/secubox-app-glances/files/etc/init.d/glances @@ -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 +} diff --git a/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl b/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl new file mode 100644 index 00000000..f8603e65 --- /dev/null +++ b/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl @@ -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 + +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://:61208 +API Endpoint: http://: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://:$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