Files
librenms-librenms/app/Http/Controllers/Maps/CustomMapDataController.php
eskyuu cb09ae0d54 Additional custom map features (#15806)
* Added options on edges for a label and to show graphs as bps as well as percentages

I think that vis.js needs to be updated to allow both bps and percentages at the same time.

* Add database migration

* Try to avoid putting multiple mid points in the same position

* Added a URL parameter for screenshot mode, where node labels are blanked out
Also fixed up the node labels in the editor

* Added legend to the editor as well as database options for reversing arrows and adjusting the edge separation

All features have been implemented in the editor, but need to be implemented in the viewer

* Fix missing defaults on the edit map list page
Added arrow reverse code to the viewer
Added legend code to the viewer
Added code to the editor to correclty handle moving the legend

* Formatting fixes and DB schema update

* Remove view from database schema
2024-02-07 11:42:35 -06:00

389 lines
16 KiB
PHP

<?php
/**
* CustomMapController.php
*
* Controller for custom maps
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2023 Steven Wilton
* @author Steven Wilton <swilton@fluentit.com.au>
*/
namespace App\Http\Controllers\Maps;
use App\Http\Controllers\Controller;
use App\Models\CustomMap;
use App\Models\CustomMapEdge;
use App\Models\CustomMapNode;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use LibreNMS\Config;
use LibreNMS\Util\Url;
class CustomMapDataController extends Controller
{
public function get(Request $request, CustomMap $map): JsonResponse
{
$this->authorize('view', $map);
$edges = [];
$nodes = [];
foreach ($map->edges as $edge) {
$edgeid = $edge->custom_map_edge_id;
$edges[$edgeid] = [
'custom_map_edge_id' => $edge->custom_map_edge_id,
'custom_map_node1_id' => $edge->custom_map_node1_id,
'custom_map_node2_id' => $edge->custom_map_node2_id,
'port_id' => $edge->port_id,
'reverse' => $edge->reverse,
'style' => $edge->style,
'showpct' => $edge->showpct,
'showbps' => $edge->showbps,
'label' => $edge->label,
'text_face' => $edge->text_face,
'text_size' => $edge->text_size,
'text_colour' => $edge->text_colour,
'mid_x' => $edge->mid_x,
'mid_y' => $edge->mid_y,
];
if ($edge->port) {
$edges[$edgeid]['device_id'] = $edge->port->device_id;
$edges[$edgeid]['port_name'] = $edge->port->device->displayName() . ' - ' . $edge->port->getLabel();
$edges[$edgeid]['port_info'] = Url::portLink($edge->port, null, null, false, true);
// Work out speed to and from
$speedto = 0;
$speedfrom = 0;
$rateto = 0;
$ratefrom = 0;
// Try to interpret the SNMP speeds
if ($edge->port->port_descr_speed) {
$speed_parts = explode('/', $edge->port->port_descr_speed, 2);
if (count($speed_parts) == 1) {
$speedto = $this->snmpSpeed($speed_parts[0]);
$speedfrom = $speedto;
} elseif ($edge->reverse) {
$speedto = $this->snmpSpeed($speed_parts[1]);
$speedfrom = $this->snmpSpeed($speed_parts[0]);
} else {
$speedto = $this->snmpSpeed($speed_parts[0]);
$speedfrom = $this->snmpSpeed($speed_parts[1]);
}
if ($speedto == 0 || $speedfrom == 0) {
$speedto = 0;
$speedfrom = 0;
}
}
// If we did not get a speed from the snmp desc, use the deteced speed
if ($speedto == 0 && $edge->port->ifSpeed) {
$speedto = $edge->port->ifSpeed;
$speedfrom = $edge->port->ifSpeed;
}
// Get the to/from rates
if ($edge->reverse) {
$ratefrom = $edge->port->ifInOctets_rate * 8;
$rateto = $edge->port->ifOutOctets_rate * 8;
} else {
$ratefrom = $edge->port->ifOutOctets_rate * 8;
$rateto = $edge->port->ifInOctets_rate * 8;
}
if ($speedto == 0) {
$edges[$edgeid]['port_topct'] = -1.0;
$edges[$edgeid]['port_frompct'] = -1.0;
} else {
$edges[$edgeid]['port_topct'] = round($rateto / $speedto * 100.0, 2);
$edges[$edgeid]['port_frompct'] = round($ratefrom / $speedfrom * 100.0, 2);
}
if ($edge->port->ifOperStatus != 'up') {
// If the port is not online, show the same as speed unknown
$edges[$edgeid]['colour_to'] = $this->speedColour(-1.0);
$edges[$edgeid]['colour_from'] = $this->speedColour(-1.0);
} else {
$edges[$edgeid]['colour_to'] = $this->speedColour($edges[$edgeid]['port_topct']);
$edges[$edgeid]['colour_from'] = $this->speedColour($edges[$edgeid]['port_frompct']);
}
$edges[$edgeid]['port_tobps'] = $this->rateString($rateto);
$edges[$edgeid]['port_frombps'] = $this->rateString($ratefrom);
$edges[$edgeid]['width_to'] = $this->speedWidth($speedto);
$edges[$edgeid]['width_from'] = $this->speedWidth($speedfrom);
}
}
foreach ($map->nodes as $node) {
$nodeid = $node->custom_map_node_id;
$nodes[$nodeid] = [
'custom_map_node_id' => $node->custom_map_node_id,
'device_id' => $node->device_id,
'linked_map_id' => $node->linked_custom_map_id,
'linked_map_name' => $node->linked_map ? $node->linked_map->name : null,
'label' => $node->label,
'style' => $node->style,
'icon' => $node->icon,
'image' => $node->image,
'size' => $node->size,
'border_width' => $node->border_width,
'text_face' => $node->text_face,
'text_size' => $node->text_size,
'text_colour' => $node->text_colour,
'colour_bg' => $node->colour_bg,
'colour_bdr' => $node->colour_bdr,
'colour_bg_view' => $node->colour_bg,
'colour_bdr_view' => $node->colour_bdr,
'x_pos' => $node->x_pos,
'y_pos' => $node->y_pos,
];
if ($node->device) {
$nodes[$nodeid]['device_name'] = $node->device->hostname . '(' . $node->device->sysName . ')';
$nodes[$nodeid]['device_image'] = $node->device->icon;
$nodes[$nodeid]['device_info'] = Url::deviceLink($node->device, null, [], 0, 0, 0, 0);
if ($node->device->disabled) {
$device_style = $this->nodeDisabledStyle();
} elseif (! $node->device->status) {
$device_style = $this->nodeDownStyle();
} else {
$device_style = $this->nodeUpStyle();
}
if ($device_style['background']) {
$nodes[$nodeid]['colour_bg_view'] = $device_style['background'];
}
if ($device_style['border']) {
$nodes[$nodeid]['colour_bdr_view'] = $device_style['border'];
}
}
}
return response()->json(['nodes' => $nodes, 'edges' => $edges]);
}
public function save(Request $request, CustomMap $map): JsonResponse
{
$this->authorize('update', $map);
$data = $this->validate($request, [
'newnodeconf' => 'array',
'newedgeconf' => 'array',
'nodes' => 'array',
'edges' => 'array',
'legend_x' => 'integer',
'legend_y' => 'integer',
]);
$map->load(['nodes', 'edges']);
DB::transaction(function () use ($map, $data) {
if ($map->legend_x != $data['legend_x'] || $map->legend_y != $data['legend_y']) {
$map->legend_x = $data['legend_x'];
$map->legend_y = $data['legend_y'];
$map->save();
}
$dbnodes = $map->nodes->keyBy('custom_map_node_id')->all();
$dbedges = $map->edges->keyBy('custom_map_edge_id')->all();
$nodesProcessed = [];
$edgesProcessed = [];
$newNodes = [];
$map->newnodeconfig = $data['newnodeconf'];
$map->newedgeconfig = $data['newedgeconf'];
$map->save();
foreach ($data['nodes'] as $nodeid => $node) {
if (strpos($nodeid, 'new') === 0) {
$dbnode = new CustomMapNode;
$dbnode->map()->associate($map);
} else {
$dbnode = $dbnodes[$nodeid];
if (! $dbnode) {
Log::error('Could not find existing node for node id ' . $nodeid);
abort(404);
}
}
$dbnode->device_id = is_numeric($node['title']) ? $node['title'] : null;
$dbnode->linked_custom_map_id = str_starts_with($node['title'], 'map:') ? (int) str_replace('map:', '', $node['title']) : null;
$dbnode->label = $node['label'];
$dbnode->style = $node['shape'];
$dbnode->icon = $node['icon'];
$dbnode->image = $node['image']['unselected'] ?? '';
$dbnode->size = $node['size'];
$dbnode->text_face = $node['font']['face'];
$dbnode->text_size = $node['font']['size'];
$dbnode->text_colour = $node['font']['color'];
$dbnode->colour_bg = $node['color']['background'] ?? null;
$dbnode->colour_bdr = $node['color']['border'] ?? null;
$dbnode->border_width = $node['borderWidth'];
$dbnode->x_pos = intval($node['x']);
$dbnode->y_pos = intval($node['y']);
$dbnode->save();
$nodesProcessed[$dbnode->custom_map_node_id] = true;
$newNodes[$nodeid] = $dbnode;
}
foreach ($data['edges'] as $edgeid => $edge) {
if (strpos($edgeid, 'new') === 0) {
$dbedge = new CustomMapEdge;
$dbedge->map()->associate($map);
} else {
$dbedge = $dbedges[$edgeid];
if (! $dbedge) {
Log::error('Could not find existing edge for edge id ' . $edgeid);
abort(404);
}
}
$dbedge->custom_map_node1_id = strpos($edge['from'], 'new') == 0 ? $newNodes[$edge['from']]->custom_map_node_id : $edge['from'];
$dbedge->custom_map_node2_id = strpos($edge['to'], 'new') == 0 ? $newNodes[$edge['to']]->custom_map_node_id : $edge['to'];
$dbedge->port_id = $edge['port_id'] ? $edge['port_id'] : null;
$dbedge->reverse = filter_var($edge['reverse'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$dbedge->showpct = filter_var($edge['showpct'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$dbedge->showbps = filter_var($edge['showbps'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$dbedge->label = $edge['label'] ? $edge['label'] : '';
$dbedge->style = $edge['style'];
$dbedge->text_face = $edge['text_face'];
$dbedge->text_size = $edge['text_size'];
$dbedge->text_colour = $edge['text_colour'];
$dbedge->mid_x = intval($edge['mid_x']);
$dbedge->mid_y = intval($edge['mid_y']);
$dbedge->save();
$edgesProcessed[$dbedge->custom_map_edge_id] = true;
}
foreach ($map->edges as $edge) {
if (! array_key_exists($edge->custom_map_edge_id, $edgesProcessed)) {
$edge->delete();
}
}
foreach ($map->nodes as $node) {
if (! array_key_exists($node->custom_map_node_id, $nodesProcessed)) {
$node->delete();
}
}
});
return response()->json(['id' => $map->custom_map_id]);
}
private function rateString(int $rate): string
{
if ($rate < 1000) {
return $rate . ' bps';
} elseif ($rate < 1000000) {
return intval($rate / 1000) . ' kbps';
} elseif ($rate < 1000000000) {
return intval($rate / 1000000) . ' Mbps';
} elseif ($rate < 1000000000000) {
return intval($rate / 1000000000) . ' Gbps';
} elseif ($rate < 1000000000000000) {
return intval($rate / 1000000000000) . ' Tbps';
} else {
return intval($rate / 1000000000000000) . ' Pbps';
}
}
private function snmpSpeed(string $speeds): int
{
// Only succeed if the string startes with a number optionally followed by a unit
if (preg_match('/^(\d+)([kMGTP])?/', $speeds, $matches)) {
$speed = (int) $matches[1];
if (count($matches) < 3) {
return $speed;
} elseif ($matches[2] == 'k') {
$speed *= 1000;
} elseif ($matches[2] == 'M') {
$speed *= 1000000;
} elseif ($matches[2] == 'G') {
$speed *= 1000000000;
} elseif ($matches[2] == 'T') {
$speed *= 1000000000000;
} elseif ($matches[2] == 'P') {
$speed *= 1000000000000000;
}
return $speed;
}
return 0;
}
private function speedColour(float $pct): string
{
// For the maths below, the 5.1 is worked out as 255 / 50
// (255 being the max colour value and 50 is the max of the $pct calcluation)
if ($pct < 0) {
// Black if we can't determine the percentage (link down or speed 0)
return '#000000';
} elseif ($pct < 50) {
// 100% green and slowly increase the red until we get to yellow
return sprintf('#%02XFF00', (int) (5.1 * $pct));
} elseif ($pct < 100) {
// 100% red and slowly remove green to go from yellow to red
return sprintf('#FF%02X00', (int) (5.1 * (100.0 - $pct)));
} elseif ($pct < 150) {
// 100% red and slowly increase blue to go purple
return sprintf('#FF00%02X', (int) (5.1 * ($pct - 100.0)));
}
// Default to purple for links over 150%
return '#FF00FF';
}
private function speedWidth(int $speed): float
{
if ($speed < 1000000) {
return 1.0;
}
return (strlen((string) $speed) - 5) / 2.0;
}
protected function nodeDisabledStyle(): array
{
return [
'border' => Config::get('network_map_legend.di.border'),
'background' => Config::get('network_map_legend.di.node'),
];
}
protected function nodeDownStyle(): array
{
return [
'border' => Config::get('network_map_legend.dn.border'),
'background' => Config::get('network_map_legend.dn.node'),
];
}
protected function nodeUpStyle(): array
{
return [
'border' => null,
'background' => null,
];
}
}