dashboard: pluggable metadata files

This commit is contained in:
Stephan de Wit 2024-06-28 14:32:14 +02:00
parent b83fc5750a
commit e0587f7bff
3 changed files with 336 additions and 198 deletions

1
plist
View File

@ -1977,6 +1977,7 @@
/usr/local/opnsense/www/js/widgets/LiveLog.js
/usr/local/opnsense/www/js/widgets/Mbuf.js
/usr/local/opnsense/www/js/widgets/Memory.js
/usr/local/opnsense/www/js/widgets/Metadata/Core.xml
/usr/local/opnsense/www/js/widgets/Monit.js
/usr/local/opnsense/www/js/widgets/Swap.js
/usr/local/opnsense/www/js/widgets/SystemInformation.js

View File

@ -31,195 +31,79 @@ namespace OPNsense\Core\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\ACL;
use OPNsense\Core\Config;
use SimpleXMLElement;
class DashboardController extends ApiControllerBase
{
private function getTranslations()
private $metadataCacheFileName = null;
private $metadataFileLocation = "/usr/local/opnsense/www/js/widgets/Metadata";
private $metadataCacheTTL = 3600;
private $acl = null;
public function __construct()
{
return [
'cpu' => [
'title' => gettext('CPU'),
'total' => gettext('Total'),
'interrupt' => gettext('Interrupt'),
'user' => gettext('User'),
'system' => gettext('System'),
],
'interfaces' => [
'title' => gettext('Interfaces'),
],
'systeminformation' => [
'title' => gettext('System Information'),
'name' => gettext('Name'),
'versions' => gettext('Versions'),
'updates' => gettext('Updates'),
'datetime' => gettext('Current date/time'),
'uptime' => gettext('Uptime'),
'config' => gettext('Last configuration change')
],
'interfacestatistics' => [
'title' => gettext('Interface Statistics'),
'bytesin' => gettext('Bytes In'),
'bytesout' => gettext('Bytes Out'),
'packetsin' => gettext('Packets In'),
'packetsout' => gettext('Packets Out'),
'errorsin' => gettext('Errors In'),
'errorsout' => gettext('Errors Out'),
'collisions' => gettext('Collisions'),
],
'traffic' => [
'title' => gettext('Traffic Graph'),
'trafficin' => gettext('Traffic In'),
'trafficout' => gettext('Traffic Out'),
],
'memory' => [
'title' => gettext('Memory usage'),
'used' => gettext('Used'),
'free' => gettext('Free'),
'arc' => gettext('ARC'),
],
'disk' => [
'title' => gettext('Disk usage'),
'used' => gettext('Used'),
'free' => gettext('Free'),
],
'wireguard' => [
'title' => gettext('Wireguard'),
'instance' => gettext('Instance'),
'peer' => gettext('Peer'),
'pubkey' => gettext('Public Key'),
'handshake' => gettext('Latest handshake'),
],
'firewall' => [
'title' => gettext('Firewall'),
'action' => gettext('Action'),
'time' => gettext('Time'),
'interface' => gettext('Interface'),
'source' => gettext('Source'),
'destination' => gettext('Destination'),
'port' => gettext('Port'),
'matchedrule' => gettext('Matched rule'),
'click' => gettext('Click to track this rule in Live View'),
'label' => gettext('Label'),
'count' => gettext('Count'),
'livelog' => gettext('Live Log'),
'events' => gettext('Events'),
'nodata' => gettext('Waiting for data')
],
'firewallstates' => [
'title' => gettext('Firewall States'),
'used' => gettext('Used'),
'free' => gettext('Free'),
],
'mbuf' => [
'title' => gettext('MBUF Usage'),
'used' => gettext('Used'),
'free' => gettext('Free'),
],
'swap' => [
'title' => gettext('SWAP Usage'),
'used' => gettext('Used'),
'free' => gettext('Free'),
],
'carp' => [
'title' => gettext('CARP Status'),
'unconfigured' => gettext('No CARP Interfaces configured. Click to configure CARP.'),
'carp' => gettext('CARP IP'),
'alias' => gettext('IP Alias'),
],
'gateways' => [
'title' => gettext('Gateways'),
'unconfigured' => gettext('No Gateways configured. Click to configure gateways.'),
'rtt' => gettext('RTT'),
'rttd' => gettext('RTTd'),
'loss' => gettext('Loss'),
],
'thermalsensors' => [
'title' => gettext('Thermal Sensors'),
'help' => gettext('CPU thermal sensors often measure the same temperature for each core. If this is the case, only the first core is shown.'),
'unconfigured' => gettext('Thermal sensors not available or not configured.')
],
'monit' => [
'title' => gettext('Monit Status'),
'filesystem' => gettext('Filesystem'),
'directory' => gettext('Directory'),
'file' => gettext('File'),
'process' => gettext('Process'),
'host' => gettext('Host'),
'system' => gettext('System'),
'fifo' => gettext('FIFO'),
'custom' => gettext('Custom'),
'network' => gettext('Network'),
'ok' => gettext('OK'),
'failed' => gettext('Failed'),
'changed' => gettext('Changed'),
'unchanged' => gettext('Not changed'),
'type' => gettext('Type'),
'unconfigured' => gettext('Monit is disabled or not configured.'),
],
'livelog' => [
'title' => gettext('Live Log'),
'time' => gettext('Time'),
'severity' => gettext('Severity'),
'process' => gettext('Process'),
'message' => gettext('Message'),
],
'ipsecleases' => [
'title' => gettext('IPsec Leases'),
'online' => gettext('Online'),
'offline' => gettext('Offline'),
'users' => gettext('Users'),
'unconfigured' => gettext('IPsec is currently disabled. Click to configure IPsec.'),
'noleases' => gettext('There are currently no leases.'),
'nodata' => gettext('Failed to load data.'),
],
'ipsectunnels' => [
'title' => gettext('IPsec Tunnels'),
'online' => gettext('Online'),
'offline' => gettext('Offline'),
'total' => gettext('Tunnels'),
'unconfigured' => gettext('IPsec is currently disabled. Click to configure IPsec.'),
'notunnels' => gettext('There are currently no tunnels.'),
'nodata' => gettext('Failed to load data.'),
'notavailable' => gettext('n/a'),
]
];
$this->metadataCacheFileName = sys_get_temp_dir() . "/opnsense_dashboard_metadata_cache.xml";
$this->acl = new ACL();
}
private function canAccessEndpoints($fname)
private function canAccessEndpoints($endpoints)
{
if (!file_exists($fname)) {
return false;
}
$handle = fopen($fname, "r");
if ($handle) {
$lines = [];
while (($line = fgets($handle)) !== false) {
if (strpos($line, "// endpoint:") === 0) {
$endpoint = explode(':', trim($line))[1] ?? null;
if (!empty($endpoint)) {
$endpoint = strstr($endpoint, ' ', true) ?: $endpoint;
$lines[] = $endpoint;
}
continue;
}
break;
}
fclose($handle);
$acl = new ACL();
foreach ($lines as $line) {
if (!$acl->isPageAccessible($this->getUserName(), $line)) {
return false;
}
foreach ($endpoints as $endpoint) {
if (!$this->acl->isPageAccessible($this->getUserName(), $endpoint)) {
return false;
}
}
return true;
}
private function getMetadata()
{
$cacheExpired = true;
if (file_exists($this->metadataCacheFileName)) {
$fstat = stat($this->metadataCacheFileName);
$cacheExpired = $this->metadataCacheTTL < (time() - $fstat['mtime']);
}
if ($cacheExpired) {
$combinedXml = new \DOMDocument('1.0');
$root = $combinedXml->createElement('metadata');
$combinedXml->appendChild($root);
foreach (glob($this->metadataFileLocation . '/*.xml') as $file) {
$metadataXml = simplexml_load_file($file);
if ($metadataXml === false) {
// not a valid xml file
continue;
}
if ($metadataXml->getName() !== "metadata") {
// wrong type
continue;
}
$node = dom_import_simplexml($metadataXml);
$node = $root->ownerDocument->importNode($node, true);
$root->appendChild($node);
}
$fp = fopen($this->metadataCacheFileName, file_exists($this->metadataCacheFileName) ? 'r+' : 'w+');
if (flock($fp, LOCK_EX | LOCK_NB)) {
ftruncate($fp, 0);
fwrite($fp, $combinedXml->saveXML());
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
chmod($this->metadataCacheFileName, 0660);
}
$combinedXml = simplexml_import_dom($combinedXml);
} else {
$combinedXml = @simplexml_load_file($this->metadataCacheFileName);
}
return $combinedXml;
}
public function getDashboardAction()
{
$this->sessionClose();
@ -233,30 +117,35 @@ class DashboardController extends ApiControllerBase
}
}
$widgetModules = array_filter(
glob('/usr/local/opnsense/www/js/widgets/*.js'),
function ($element) {
$base = basename($element);
if (str_contains($base, '.js') && !str_contains($base, 'Base')) {
return $this->canAccessEndpoints($element);
$result['modules'] = [];
$metadata = $this->getMetadata();
foreach ($metadata as $md) {
foreach($md as $widgetId => $metadataAttributes) {
$widgetId = (string)$widgetId;
$fname = (string)$metadataAttributes->filename;
$endpoints = (array)($metadataAttributes->endpoints->endpoint ?? []);
$translations = (array)($metadataAttributes->translations ?? []);
error_log(print_r($translations, true));
if(!$this->canAccessEndpoints($endpoints)) {
continue;
}
return false;
if (!file_exists('/usr/local/opnsense/www/js/widgets/' . $fname)) {
continue;
}
foreach($translations as $key => $value) {
$translations[$key] = gettext($value);
}
$result['modules'][] = [
'id' => $widgetId,
'module' => $fname,
'translations' => $translations
];
}
);
$widgetModules = array_map(function ($element) {
return basename($element);
}, $widgetModules);
$result['modules'] = [];
foreach ($widgetModules as $module) {
$id = strtolower(basename($module, '.js'));
$result['modules'][] = [
'id' => $id,
'module' => basename($module),
'translations' => $this->getTranslations()[$id] ?? []
];
}
$result['dashboard'] = !empty($dashboard) ? base64_decode($dashboard) : null;
@ -266,7 +155,6 @@ class DashboardController extends ApiControllerBase
public function saveWidgetsAction()
{
$result = ['result' => 'failed'];
if ($this->request->isPost() && !empty($this->request->getRawBody())) {

View File

@ -0,0 +1,249 @@
<metadata>
<systeminformation>
<filename>SystemInformation.js</filename>
<endpoints>
<endpoint>/api/core/system/systemInformation</endpoint>
</endpoints>
<translations>
<title>System Information</title>
<name>name</name>
<versions>Versions</versions>
<updates>Updates</updates>
<datetime>Current date/time</datetime>
<uptime>Uptime</uptime>
<config>Last configuration change</config>
</translations>
</systeminformation>
<interfaces>
<filename>Interfaces.js</filename>
<endpoints>
<endpoint>/api/interfaces/overview/*</endpoint>
</endpoints>
<translations>
<title>Interfaces</title>
</translations>
</interfaces>
<cpu>
<filename>Cpu.js</filename>
<endpoints>
<endpoint>/api/diagnostics/cpu_usage/*</endpoint>
</endpoints>
<translations>
<title>CPU</title>
<total>Total</total>
<user>User</user>
<system>System</system>
<interrupt>Interrupt</interrupt>
</translations>
</cpu>
<interfacestatistics>
<filename>InterfaceStatistics.js</filename>
<endpoints>
<endpoint>/api/interfaces/statistics/*</endpoint>
</endpoints>
<translations>
<title>Interface Statistics</title>
<bytesin>Bytes In</bytesin>
<bytesout>Bytes Out</bytesout>
<packetsin>Packets In</packetsin>
<packetsout>Packets Out</packetsout>
<errorsin>Errors In</errorsin>
<errorsout>Errors Out</errorsout>
<collisions>Collisions</collisions>
</translations>
</interfacestatistics>
<traffic>
<filename>Traffic.js</filename>
<endpoints>
<endpoint>/api/diagnostics/traffic/*</endpoint>
</endpoints>
<translations>
<title>Traffic Graph</title>
<trafficin>Traffic In</trafficin>
<trafficout>Traffic Out</trafficout>
</translations>
</traffic>
<memory>
<filename>Memory.js</filename>
<endpoints>
<endpoint>/api/core/system/systemResources</endpoint>
</endpoints>
<translations>
<title>Memory usage</title>
<used>Used</used>
<free>Free</free>
<arc>ARC</arc>
</translations>
</memory>
<disk>
<filename>Disk.js</filename>
<endpoints>
<endpoint>/api/core/system/systemDisk</endpoint>
</endpoints>
<translations>
<title>Disk usage</title>
<used>Used</used>
<free>Free</free>
</translations>
</disk>
<firewall>
<filename>Firewall.js</filename>
<endpoints>
<endpoint>/api/diagnostics/firewall/streamLog</endpoint>
<endpoint>/api/diagnostics/interface/getInterfaceNames</endpoint>
</endpoints>
<translations>
<title>Firewall</title>
<action>Action</action>
<time>Time</time>
<interface>Interface</interface>
<source>Source</source>
<destination>Destination</destination>
<port>Port</port>
<matchedrule>Matched rule</matchedrule>
<click>Click to track this rule in Live View</click>
<label>Label</label>
<count>Count</count>
<livelog>Live Log</livelog>
<events>Events</events>
<nodata>Waiting for data</nodata>
</translations>
</firewall>
<firewallstates>
<filename>FirewallStates.js</filename>
<endpoints>
<endpoint>/api/diagnostics/firewall/pf_states</endpoint>
</endpoints>
<translations>
<title>Firewall States</title>
<used>Used</used>
<free>Free</free>
</translations>
</firewallstates>
<mbuf>
<filename>Mbuf.js</filename>
<endpoints>
<endpoint>/api/core/system/system_mbuf</endpoint>
</endpoints>
<translations>
<title>Mbuf Usage</title>
<used>Used</used>
<free>Free</free>
</translations>
</mbuf>
<swap>
<filename>Swap.js</filename>
<endpoints>
<endpoint>/api/core/system/system_swap</endpoint>
</endpoints>
<translations>
<title>Swap Usage</title>
<used>Used</used>
<free>Free</free>
</translations>
</swap>
<carp>
<filename>CARP Status</filename>
<endpoints>
<endpoint>/api/diagnostics/interface/get_vip_status</endpoint>
</endpoints>
<translations>
<title>CARP Status</title>
<unconfigured>No CARP Interfaces configured. Click to configure CARP.</unconfigured>
<carp>CARP IP</carp>
<alias>IP Alias</alias>
</translations>
</carp>
<gateways>
<filename>Gateways.js</filename>
<endpoints>
<endpoint>/api/routes/gateway/status</endpoint>
</endpoints>
<translations>
<title>Gateways</title>
<unconfigured>No Gateways configured. Click to configure gateways.</unconfigured>
<rtt>RTT</rtt>
<rttd>RTTd</rttd>
<loss>Loss</loss>
</translations>
</gateways>
<thermalsensors>
<filename>ThermalSensors.js</filename>
<endpoints>
<endpoint>/api/core/system/systemTemperature</endpoint>
</endpoints>
<translations>
<title>Thermal Sensors</title>
<help>CPU thermal sensors often measure the same temperature for each core. If this is the case, only the first core is shown.</help>
<unconfigured>Thermal sensors not available or not configured.</unconfigured>
</translations>
</thermalsensors>
<monit>
<filename>Monit.js</filename>
<endpoints>
<endpoint>/api/monit/status/get/xml</endpoint>
</endpoints>
<translations>
<title>Monit Status</title>
<filesystem>Filesystem</filesystem>
<directory>Directory</directory>
<file>File</file>
<process>Process</process>
<host>Host</host>
<system>System</system>
<fifo>FIFO</fifo>
<custom>Custom</custom>
<network>Network</network>
<ok>OK</ok>
<failed>Failed</failed>
<changed>Changed</changed>
<unchanged>Not changed</unchanged>
<type>Type</type>
<unconfigured>Monit is disabled or not configured.</unconfigured>
</translations>
</monit>
<livelog>
<filename>LiveLog.js</filename>
<endpoints>
<endpoint>/api/diagnostics/log/*</endpoint>
</endpoints>
<translations>
<title>Live Log</title>
<time>Time</time>
<severity>Severity</severity>
<process>Process</process>
<message>Message</message>
</translations>
</livelog>
<ipsecleases>
<filename>IpsecLeases.js</filename>
<endpoints>
<endpoint>/api/ipsec/*</endpoint>
</endpoints>
<translations>
<title>IPsec Leases</title>
<online>Online</online>
<offline>Offline</offline>
<users>Users</users>
<unconfigured>IPsec is currently disabled. Click to configure IPsec.</unconfigured>
<noleases>There are currently no leases.</noleases>
<nodata>Failed to load data.</nodata>
</translations>
</ipsecleases>
<ipsectunnels>
<filename>IpsecTunnels.js</filename>
<endpoints>
<endpoint>/api/ipsec/*</endpoint>
</endpoints>
<translations>
<title>IPsec Tunnels</title>
<online>Online</online>
<offline>Offline</offline>
<total>Tunnels</total>
<unconfigured>IPsec is currently disabled. Click to configure IPsec.</unconfigured>
<notunnels>There are currently no tunnels.</notunnels>
<nodata>Failed to load data.</nodata>
<notavailable>N/A</notavailable>
</translations>
</ipsectunnels>
</metadata>