From c80127f5f82189d4a1c97b5be086d5cebb015637 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Tue, 30 Jun 2026 16:59:21 -0500 Subject: [PATCH 01/16] New Graph Interface --- LibreNMS/Data/Graphing/GraphFactory.php | 44 ++++ LibreNMS/Data/Graphing/LegacyGraph.php | 191 ++++++++++++++++++ LibreNMS/Exceptions/InvalidGraph.php | 36 ++++ .../Data/Graphing/GraphInterface.php | 36 ++++ app/Console/Commands/GraphDetail.php | 37 ++++ 5 files changed, 344 insertions(+) create mode 100644 LibreNMS/Data/Graphing/GraphFactory.php create mode 100644 LibreNMS/Data/Graphing/LegacyGraph.php create mode 100644 LibreNMS/Exceptions/InvalidGraph.php create mode 100644 LibreNMS/Interfaces/Data/Graphing/GraphInterface.php create mode 100644 app/Console/Commands/GraphDetail.php diff --git a/LibreNMS/Data/Graphing/GraphFactory.php b/LibreNMS/Data/Graphing/GraphFactory.php new file mode 100644 index 00000000000..7b9198e8652 --- /dev/null +++ b/LibreNMS/Data/Graphing/GraphFactory.php @@ -0,0 +1,44 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing; + +use LibreNMS\Exceptions\InvalidGraph; +use LibreNMS\Interfaces\Data\Graphing\GraphInterface; + +class GraphFactory +{ + /** + * @throws InvalidGraph + */ + public function graphFor(string $name): GraphInterface + { + if (! preg_match('/([a-z]+)_([a-zA-Z0-9_]+)/', $name, $matches)) { + throw new InvalidGraph; + } + + return new LegacyGraph($matches[1], $matches[2]); // Only legacy for now + } +} diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php new file mode 100644 index 00000000000..047cad908a4 --- /dev/null +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -0,0 +1,191 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing; + +use App\Facades\DeviceCache; +use App\Models\Device; +use App\Models\Port; +use App\Models\User; +use LibreNMS\Exceptions\InvalidGraph; +use LibreNMS\Interfaces\Data\Graphing\GraphInterface; + +class LegacyGraph implements GraphInterface +{ + private readonly string $auth_file; + private readonly string $graph_file; + + private ?string $pageTitle = null; + private ?string $graphTitle = null; + private ?bool $authorized = null; + private array $rrdFiles = []; + + public function __construct( + public readonly string $type, + public readonly string $subtype, + private ?Device $device = null, + private ?Port $port = null, + ) { + $this->device ??= DeviceCache::getPrimary(); + + $this->auth_file = base_path("includes/html/graphs/$this->type/auth.inc.php"); + $graph_file = base_path("includes/html/graphs/$this->type/$this->subtype.inc.php"); + if (! file_exists($graph_file)) { + $graph_file = base_path("includes/html/graphs/$this->type/generic.inc.php"); + } + $this->graph_file = $graph_file; + + if (! file_exists($this->auth_file) || ! file_exists($this->graph_file)) { + throw new InvalidGraph; + } + } + + public function authorize(): bool + { + if ($this->authorized === null) { + $this->legacyIncludes(); + auth()->setUser(User::firstWhere('username', 'murrant')); // FIXME + $device = $this->device; + + $auth = false; + include $this->auth_file; + + $this->authorized = $auth; + $this->graphTitle = $graph_title ?? $this->graphTitle; + $this->pageTitle = $title ?? $this->graphTitle; + + $unhandled = array_diff(array_keys(get_defined_vars()), ['auth', 'device', 'title', 'graph_title']); + if ($unhandled) { + dd($unhandled); + } + } + + return $this->authorized; + } + + public function validation(): array + { + return []; + } + + public function definition(array $vars = []): array + { + $this->legacyIncludes(); + + // set up "global" variables + $device = $this->device; + $graph_title = $this->graphTitle; + $graph_params = new GraphParameters($vars); + $type = $graph_params->type; + $subtype = $graph_params->subtype; + $height = $graph_params->height; + $width = $graph_params->width; + $from = $graph_params->from; + $to = $graph_params->to; + $period = $graph_params->period; + $prev_from = $graph_params->prev_from; + $inverse = $graph_params->inverse; + $in = $graph_params->in; + $out = $graph_params->out; + $float_precision = $graph_params->float_precision; + $title = $graph_params->visible('title'); + $nototal = ! $graph_params->visible('total'); + $nodetails = ! $graph_params->visible('details'); + $noagg = ! $graph_params->visible('aggregate'); + + $rrd_options = []; + + include $this->graph_file; + + if (isset($rrd_list) && is_array($rrd_list)) { + $this->rrdFiles = array_column($rrd_list, 'filename'); + } elseif (isset($rrd_filenames) && is_array($rrd_filenames)) { + $this->rrdFiles = $rrd_filenames; + } else { + $this->rrdFiles = isset($rrd_filename) ? [$rrd_filename] : []; + } + +// $unhandled = array_diff(array_keys(get_defined_vars()), [ +// 'vars', +// 'device', +// 'graph_title', +// 'graph_params', +// 'type', +// 'subtype', +// 'height', +// 'width', +// 'from', +// 'to', +// 'period', +// 'prev_from', +// 'inverse', +// 'in', +// 'out', +// 'float_precision', +// 'title', +// 'nototal', +// 'nodetails', +// 'noagg', +// 'rrd_options', +// 'rrd_filename', +// ]); +// if ($unhandled) { +// dd($unhandled); +// } + + return $rrd_options; + } + + public function getPageTitle(): string + { + return $this->pageTitle ?? $this->getGraphTitle(); + } + + public function getGraphTitle(): string + { + if ($this->graphTitle !== null) { + return $this->graphTitle; + } + + if ($this->port) { + return $this->port->device?->display . ' :: ' . $this->port->getDescription(); + } + + return $this->device->display ?? ''; + } + + public function getRrdFiles(): array + { + return $this->rrdFiles; + } + + private function legacyIncludes(): void + { + include_once base_path('includes/common.php'); + include_once base_path('includes/html/functions.inc.php'); + include_once base_path('includes/dbFacile.php'); + include_once base_path('includes/rewrites.php'); + } +} diff --git a/LibreNMS/Exceptions/InvalidGraph.php b/LibreNMS/Exceptions/InvalidGraph.php new file mode 100644 index 00000000000..6bb3ff163fa --- /dev/null +++ b/LibreNMS/Exceptions/InvalidGraph.php @@ -0,0 +1,36 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Exceptions; + +use Throwable; + +class InvalidGraph extends RrdException +{ +public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) +{ + parent::__construct($message ?: "Invalid Graph", $code, $previous); +} +} diff --git a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php new file mode 100644 index 00000000000..266c4cc70b4 --- /dev/null +++ b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php @@ -0,0 +1,36 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Interfaces\Data\Graphing; + +interface GraphInterface +{ + public function authorize(): bool; + public function validation(): array; + public function definition(array $vars = []): array; + public function getPageTitle(): string; + public function getGraphTitle(): string; + public function getRrdFiles(): array; +} diff --git a/app/Console/Commands/GraphDetail.php b/app/Console/Commands/GraphDetail.php new file mode 100644 index 00000000000..bc744c20a5f --- /dev/null +++ b/app/Console/Commands/GraphDetail.php @@ -0,0 +1,37 @@ +option('device') ?: Device::limit(1)->value('device_id')); + DeviceCache::setPrimary($device->device_id); + + $graph = $graphs->graphFor($this->argument('name')); + $def = $graph->definition([ + 'type' => $this->argument('name'), + ]); + + + $this->line("Type: $graph->type"); + $this->line("Subtype: $graph->subtype"); + $this->line('Authorized: ' . ($graph->authorize() ? 'true' : 'false')); + $this->line("Graph Title: " . $graph->getGraphTitle()); + $this->line("Page Title: " . substr($graph->getPageTitle(), 0, 80)); + $this->line("Rrd Files: " . implode(', ', array_map(fn($f) => basename($f), $graph->getRrdFiles()))); + $this->line("Definition: " . substr(implode(' ', $def), 80)); + + + return 0; + } +} From db734017caec85cc668bf6349e140df49d449065 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Tue, 30 Jun 2026 17:38:55 -0500 Subject: [PATCH 02/16] Refactors to support auth defining variables more easily --- LibreNMS/Data/Graphing/GraphFactory.php | 6 +- LibreNMS/Data/Graphing/LegacyGraph.php | 140 ++++++++++++------------ LibreNMS/Util/Graph.php | 117 ++++++++------------ app/Console/Commands/GraphDetail.php | 8 +- 4 files changed, 122 insertions(+), 149 deletions(-) diff --git a/LibreNMS/Data/Graphing/GraphFactory.php b/LibreNMS/Data/Graphing/GraphFactory.php index 7b9198e8652..d2c1b77e06b 100644 --- a/LibreNMS/Data/Graphing/GraphFactory.php +++ b/LibreNMS/Data/Graphing/GraphFactory.php @@ -33,12 +33,14 @@ class GraphFactory /** * @throws InvalidGraph */ - public function graphFor(string $name): GraphInterface + public function graphFor(string $name, array $vars = []): GraphInterface { if (! preg_match('/([a-z]+)_([a-zA-Z0-9_]+)/', $name, $matches)) { throw new InvalidGraph; } - return new LegacyGraph($matches[1], $matches[2]); // Only legacy for now + $vars['type'] ??= $name; + + return new LegacyGraph($matches[1], $matches[2], $vars); // Only legacy for now } } diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index 047cad908a4..19e919dda69 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -28,9 +28,9 @@ use App\Facades\DeviceCache; use App\Models\Device; use App\Models\Port; -use App\Models\User; use LibreNMS\Exceptions\InvalidGraph; use LibreNMS\Interfaces\Data\Graphing\GraphInterface; +use function base_path; class LegacyGraph implements GraphInterface { @@ -41,15 +41,19 @@ class LegacyGraph implements GraphInterface private ?string $graphTitle = null; private ?bool $authorized = null; private array $rrdFiles = []; - + private bool $loaded = false; + private array $rrdOptions = []; + private ?Device $device = null; + private ?Port $port = null; + + /** + * @throws InvalidGraph + */ public function __construct( public readonly string $type, public readonly string $subtype, - private ?Device $device = null, - private ?Port $port = null, + private array $vars = [], ) { - $this->device ??= DeviceCache::getPrimary(); - $this->auth_file = base_path("includes/html/graphs/$this->type/auth.inc.php"); $graph_file = base_path("includes/html/graphs/$this->type/$this->subtype.inc.php"); if (! file_exists($graph_file)) { @@ -62,41 +66,48 @@ public function __construct( } } - public function authorize(): bool + private function load(array $vars = []): void { - if ($this->authorized === null) { - $this->legacyIncludes(); - auth()->setUser(User::firstWhere('username', 'murrant')); // FIXME - $device = $this->device; - - $auth = false; - include $this->auth_file; - - $this->authorized = $auth; - $this->graphTitle = $graph_title ?? $this->graphTitle; - $this->pageTitle = $title ?? $this->graphTitle; - - $unhandled = array_diff(array_keys(get_defined_vars()), ['auth', 'device', 'title', 'graph_title']); - if ($unhandled) { - dd($unhandled); - } + if ($this->loaded && empty($vars)) { + return; } - return $this->authorized; - } + $this->vars = array_merge($this->vars, $vars); - public function validation(): array - { - return []; - } + include_once base_path('includes/common.php'); + include_once base_path('includes/html/functions.inc.php'); + include_once base_path('includes/dbFacile.php'); + include_once base_path('includes/rewrites.php'); - public function definition(array $vars = []): array - { - $this->legacyIncludes(); + if (! $this->device && isset($this->vars['device'])) { + $this->device = DeviceCache::get(is_numeric($this->vars['device']) ? $this->vars['device'] : getidbyname($this->vars['device'])); + } + + if ($this->device) { + DeviceCache::setPrimary($this->device->device_id); + } - // set up "global" variables + if (! $this->port && isset($this->vars['id']) && $this->type === 'port') { + $this->port = \App\Facades\PortCache::get($this->vars['id']); + } + + // Local scope variables for the included files $device = $this->device; - $graph_title = $this->graphTitle; + $port = $this->port; + $vars = $this->vars; + + $auth = auth()->guest(); + include $this->auth_file; + + $this->authorized = $auth; + $this->graphTitle = $graph_title ?? $this->graphTitle; + $this->pageTitle = $title ?? $this->graphTitle; + + if (! $auth) { + $this->loaded = true; + return; + } + $graph_params = new GraphParameters($vars); $type = $graph_params->type; $subtype = $graph_params->subtype; @@ -119,6 +130,8 @@ public function definition(array $vars = []): array include $this->graph_file; + $this->rrdOptions = $rrd_options; + if (isset($rrd_list) && is_array($rrd_list)) { $this->rrdFiles = array_column($rrd_list, 'filename'); } elseif (isset($rrd_filenames) && is_array($rrd_filenames)) { @@ -127,44 +140,36 @@ public function definition(array $vars = []): array $this->rrdFiles = isset($rrd_filename) ? [$rrd_filename] : []; } -// $unhandled = array_diff(array_keys(get_defined_vars()), [ -// 'vars', -// 'device', -// 'graph_title', -// 'graph_params', -// 'type', -// 'subtype', -// 'height', -// 'width', -// 'from', -// 'to', -// 'period', -// 'prev_from', -// 'inverse', -// 'in', -// 'out', -// 'float_precision', -// 'title', -// 'nototal', -// 'nodetails', -// 'noagg', -// 'rrd_options', -// 'rrd_filename', -// ]); -// if ($unhandled) { -// dd($unhandled); -// } - - return $rrd_options; + $this->loaded = true; + } + + public function authorize(): bool + { + $this->load(); + return $this->authorized; + } + + public function validation(): array + { + return []; + } + + public function definition(array $vars = []): array + { + $this->load($vars); + return $this->rrdOptions; } public function getPageTitle(): string { + $this->load(); return $this->pageTitle ?? $this->getGraphTitle(); } public function getGraphTitle(): string { + $this->load(); + if ($this->graphTitle !== null) { return $this->graphTitle; } @@ -178,14 +183,7 @@ public function getGraphTitle(): string public function getRrdFiles(): array { + $this->load(); return $this->rrdFiles; } - - private function legacyIncludes(): void - { - include_once base_path('includes/common.php'); - include_once base_path('includes/html/functions.inc.php'); - include_once base_path('includes/dbFacile.php'); - include_once base_path('includes/rewrites.php'); - } } diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index 65a812c82ac..f745c4e0920 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -26,14 +26,15 @@ namespace LibreNMS\Util; -use App\Facades\DeviceCache; use App\Facades\LibrenmsConfig; use App\Models\Device; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Auth; +use Illuminate\Validation\ValidationException; +use LibreNMS\Data\Graphing\GraphFactory; use LibreNMS\Data\Graphing\GraphImage; use LibreNMS\Data\Graphing\GraphParameters; use LibreNMS\Enum\ImageFormat; +use LibreNMS\Exceptions\InvalidGraph; use LibreNMS\Exceptions\RrdGraphException; use Rrd; @@ -99,24 +100,36 @@ public static function getImage($vars): GraphImage * @return GraphImage * * @throws RrdGraphException + * @throws InvalidGraph */ - public static function get($vars): GraphImage + public static function get(array|string $vars): GraphImage { - $graph_params = new GraphParameters(is_string($vars) ? Url::parseLegacyPathVars($vars) : $vars); + // handle possible graph url input + if (is_string($vars)) { + $vars = Url::parseLegacyPathVars($vars); + } + + $graph_params = new GraphParameters($vars); + $name = $graph_params->type . '_' . $graph_params->subtype; + + /** @var GraphFactory $factory */ + $factory = app(GraphFactory::class); + $graph = $factory->graphFor($name, $vars); + $rrd_options = self::getRrdOptions($vars, $rrd_filename); // Generating the graph! try { $image_data = Rrd::graph($rrd_options); - return new GraphImage($graph_params->imageFormat, $graph_params->getTitle(), $image_data); + return new GraphImage($graph_params->imageFormat, $graph->getGraphTitle(), $image_data); } catch (RrdGraphException $e) { // preserve original error if debug is enabled, otherwise make it a little more user friendly if (Debug::isEnabled()) { throw $e; } - if (isset($rrd_filename) && ! Rrd::checkRrdExists($rrd_filename)) { + if ($rrd_filename && ! Rrd::checkRrdExists($rrd_filename)) { throw new RrdGraphException('No Data file' . basename($rrd_filename), 'No Data', $graph_params->width, $graph_params->height, $e->getCode(), $e->getImage()); } @@ -132,82 +145,44 @@ public static function get($vars): GraphImage * @return array * * @throws RrdGraphException + * @throws InvalidGraph */ - public static function getRrdOptions($vars, ?string &$rrd_filename = null): array + public static function getRrdOptions(array|string $vars, ?string &$rrd_filename = null): array { - if (! defined('IGNORE_ERRORS')) { - define('IGNORE_ERRORS', true); + // handle possible graph url input + if (is_string($vars)) { + $vars = Url::parseLegacyPathVars($vars); } - $previousCwd = getcwd(); - chdir(base_path()); - - try { - include_once base_path('includes/dbFacile.php'); - include_once base_path('includes/common.php'); - include_once base_path('includes/html/functions.inc.php'); - include_once base_path('includes/rewrites.php'); - - // handle possible graph url input - if (is_string($vars)) { - $vars = Url::parseLegacyPathVars($vars); - } - - // variables for included graphs - $graph_params = new GraphParameters($vars); - $type = $graph_params->type; - $subtype = $graph_params->subtype; + $graph_params = new GraphParameters($vars); + $name = $graph_params->type . '_' . $graph_params->subtype; - $deviceId = $vars['device'] ?? ($type === 'device' ? ($vars['id'] ?? null) : null); - if ($deviceId) { - $device = DeviceCache::get($deviceId); - DeviceCache::setPrimary($device->device_id); - } + /** @var GraphFactory $factory */ + $factory = app(GraphFactory::class); + $graph = $factory->graphFor($name, $vars); - $height = $graph_params->height; - $width = $graph_params->width; - $from = $graph_params->from; - $to = $graph_params->to; - $period = $graph_params->period; - $prev_from = $graph_params->prev_from; - $inverse = $graph_params->inverse; - $in = $graph_params->in; - $out = $graph_params->out; - $float_precision = $graph_params->float_precision; - $title = $graph_params->visible('title'); - $nototal = ! $graph_params->visible('total'); - $nodetails = ! $graph_params->visible('details'); - $noagg = ! $graph_params->visible('aggregate'); - - $rrd_options = []; - $rrd_filename = null; - - $auth = Auth::guest(); // if user not logged in, assume we authenticated via signed url, allow_unauth_graphs or allow_unauth_graphs_cidr - require base_path("/includes/html/graphs/$type/auth.inc.php"); - if (! $auth) { - // We are unauthenticated :( - throw new RrdGraphException('No Authorization', 'No Auth', $width, $height); + // Run validation if rules are defined + if ($rules = $graph->validation()) { + $validator = \Validator::make($vars, $rules); + if ($validator->fails()) { + throw new ValidationException($validator); } + } - if (is_file(base_path("/includes/html/graphs/$type/$subtype.inc.php"))) { - require base_path("/includes/html/graphs/$type/$subtype.inc.php"); - } elseif (is_file(base_path("/includes/html/graphs/$type/generic.inc.php"))) { - require base_path("/includes/html/graphs/$type/generic.inc.php"); - } else { - throw new RrdGraphException("{$type}_$subtype template missing", "{$type}_$subtype missing", $width, $height); - } + if (! $graph->authorize()) { + throw new RrdGraphException('No Authorization', 'No Auth', $graph_params->width, $graph_params->height); + } - if (empty($rrd_options)) { // @phpstan-ignore empty.variable ($rrd_options is populated by included graph templates) - throw new RrdGraphException('Graph Definition Error', 'Def Error', $width, $height); - } + $rrd_options = $graph->definition($vars); - // @phpstan-ignore deadCode.unreachable ($rrd_options is populated by included graph templates, so this is reachable) - return [...$graph_params->toRrdOptions(), ...$rrd_options]; - } finally { - if ($previousCwd !== false) { - chdir($previousCwd); - } + if (empty($rrd_options)) { + throw new RrdGraphException('Graph Definition Error', 'Def Error', $graph_params->width, $graph_params->height); } + + $rrdFiles = $graph->getRrdFiles(); + $rrd_filename = reset($rrdFiles) ?: null; + + return [...$graph_params->toRrdOptions(), ...$rrd_options]; } public static function getTypes(): array diff --git a/app/Console/Commands/GraphDetail.php b/app/Console/Commands/GraphDetail.php index bc744c20a5f..7f6c1ec554a 100644 --- a/app/Console/Commands/GraphDetail.php +++ b/app/Console/Commands/GraphDetail.php @@ -15,21 +15,19 @@ class GraphDetail extends Command public function handle(GraphFactory $graphs): int { $device = DeviceCache::get($this->option('device') ?: Device::limit(1)->value('device_id')); - DeviceCache::setPrimary($device->device_id); - $graph = $graphs->graphFor($this->argument('name')); - $def = $graph->definition([ + $graph = $graphs->graphFor($this->argument('name'), [ 'type' => $this->argument('name'), + 'device' => $device->device_id, ]); - $this->line("Type: $graph->type"); $this->line("Subtype: $graph->subtype"); $this->line('Authorized: ' . ($graph->authorize() ? 'true' : 'false')); $this->line("Graph Title: " . $graph->getGraphTitle()); $this->line("Page Title: " . substr($graph->getPageTitle(), 0, 80)); $this->line("Rrd Files: " . implode(', ', array_map(fn($f) => basename($f), $graph->getRrdFiles()))); - $this->line("Definition: " . substr(implode(' ', $def), 80)); + $this->line("Definition: " . substr(implode(' ', $graph->definition()), 80)); return 0; From 157603b8ccc0310de4956ba51ff5d9bc27283e48 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Tue, 30 Jun 2026 18:21:32 -0500 Subject: [PATCH 03/16] port device processor graph --- LibreNMS/Data/Graphing/AbstractGraph.php | 58 ++++ .../Builders/MultiLineGraphBuilder.php | 186 +++++++++++++ .../MultiSimplexSeparatedGraphBuilder.php | 251 ++++++++++++++++++ .../Data/Graphing/Device/ProcessorGraph.php | 144 ++++++++++ LibreNMS/Data/Graphing/GraphFactory.php | 11 +- LibreNMS/Data/Graphing/LegacyGraph.php | 7 +- LibreNMS/Util/Graph.php | 5 + LibreNMS/Util/Rewrite.php | 17 ++ 8 files changed, 672 insertions(+), 7 deletions(-) create mode 100644 LibreNMS/Data/Graphing/AbstractGraph.php create mode 100644 LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php create mode 100644 LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php create mode 100644 LibreNMS/Data/Graphing/Device/ProcessorGraph.php diff --git a/LibreNMS/Data/Graphing/AbstractGraph.php b/LibreNMS/Data/Graphing/AbstractGraph.php new file mode 100644 index 00000000000..e4a9fe9f246 --- /dev/null +++ b/LibreNMS/Data/Graphing/AbstractGraph.php @@ -0,0 +1,58 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing; + +use LibreNMS\Interfaces\Data\Graphing\GraphInterface; + +abstract class AbstractGraph implements GraphInterface +{ + public function validation(): array + { + return []; + } + + public function getPageTitle(): string + { + return $this->getGraphTitle(); + } + + /** + * Bind validated parameters directly to class properties + */ + public function fill(array $vars): self + { + $reflector = new \ReflectionClass($this); + foreach ($vars as $key => $value) { + if ($reflector->hasProperty($key)) { + $property = $reflector->getProperty($key); + if ($property->isPublic() && ! $property->isReadOnly()) { + $this->$key = $value; + } + } + } + return $this; + } +} diff --git a/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php new file mode 100644 index 00000000000..f5cdebefbe6 --- /dev/null +++ b/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php @@ -0,0 +1,186 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing\Builders; + +use App\Facades\LibrenmsConfig; +use LibreNMS\Data\Graphing\GraphParameters; +use LibreNMS\Data\Store\Rrd; + +class MultiLineGraphBuilder +{ + private string $unitText = ''; + private string $units = ''; + private string $totalUnits = '%'; + private string $colours = 'mixed'; + private ?float $scaleMin = null; + private ?float $scaleMax = null; + private bool $nototal = false; + private int $descrLen = 12; + + private array $datasets = []; + + public function unitText(string $unitText): self + { + $this->unitText = $unitText; + return $this; + } + + public function units(string $units): self + { + $this->units = $units; + return $this; + } + + public function totalUnits(string $totalUnits): self + { + $this->totalUnits = $totalUnits; + return $this; + } + + public function colours(string $colours): self + { + $this->colours = $colours; + return $this; + } + + public function scaleMin(float $scaleMin): self + { + $this->scaleMin = $scaleMin; + return $this; + } + + public function scaleMax(float $scaleMax): self + { + $this->scaleMax = $scaleMax; + return $this; + } + + public function noTotal(bool $noTotal = true): self + { + $this->nototal = $noTotal; + return $this; + } + + public function descrLen(int $descrLen): self + { + $this->descrLen = $descrLen; + return $this; + } + + public function addDataset( + string $filename, + string $ds, + string $description, + ?string $colour = null, + bool $area = false, + ?string $areaColour = null, + bool $invert = false + ): self { + $this->datasets[] = [ + 'filename' => $filename, + 'ds' => $ds, + 'descr' => $description, + 'colour' => $colour, + 'area' => $area, + 'areacolour' => $areaColour, + 'invert' => $invert, + ]; + return $this; + } + + public function build(array $vars): array + { + $graph_params = new GraphParameters($vars); + $float_precision = $graph_params->float_precision; + + if ($this->scaleMin !== null) { + $graph_params->scale_min = (int) $this->scaleMin; + } + if ($this->scaleMax !== null) { + $graph_params->scale_max = (int) $this->scaleMax; + } + + $descr_len = $this->descrLen; + if ($this->nototal) { + $descr_len += 2; + } + + $rrd_options = []; + $rrd_options[] = 'COMMENT:' . Rrd::fixedSafeDescr($this->unitText, $descr_len) . " Now Min Max Avg\l"; + + $stackedVal = LibrenmsConfig::get('webui.graph_stacked') ? '1' : '-1'; + $rrd_optionsb = []; + $colour_iter = 0; + + foreach ($this->datasets as $i => $rrd) { + $colour = $rrd['colour']; + if ($colour === null) { + if (! LibrenmsConfig::get("graph_colours.{$this->colours}.{$colour_iter}")) { + $colour_iter = 0; + } + $colour = LibrenmsConfig::get("graph_colours.{$this->colours}.{$colour_iter}"); + $colour_iter++; + } + + $areacolour = $rrd['areacolour']; + if ($rrd['area'] && empty($areacolour)) { + $areacolour = $colour . '20'; + } + + $ds = $rrd['ds']; + $filename = $rrd['filename']; + $descr = Rrd::fixedSafeDescr($rrd['descr'], $descr_len); + $id = 'ds' . $i; + + $rrd_options[] = 'DEF:' . $id . "=$filename:$ds:AVERAGE"; + $rrd_options[] = 'DEF:' . $id . "min=$filename:$ds:MIN"; + $rrd_options[] = 'DEF:' . $id . "max=$filename:$ds:MAX"; + + if ($rrd['invert']) { + $rrd_options[] = 'CDEF:' . $id . 'i=' . $id . ',' . $stackedVal . ',*'; + $rrd_optionsb[] = 'LINE1.25:' . $id . 'i#' . $colour . ":$descr"; + if (! empty($areacolour)) { + $rrd_optionsb[] = 'AREA:' . $id . 'i#' . $areacolour; + } + } else { + $rrd_optionsb[] = 'LINE1.25:' . $id . '#' . $colour . ":$descr"; + if (! empty($areacolour)) { + $rrd_optionsb[] = 'AREA:' . $id . '#' . $areacolour; + } + } + + $rrd_optionsb[] = 'GPRINT:' . $id . ':LAST:%5.' . $float_precision . 'lf%s' . $this->units; + $rrd_optionsb[] = 'GPRINT:' . $id . 'min:MIN:%5.' . $float_precision . 'lf%s' . $this->units; + $rrd_optionsb[] = 'GPRINT:' . $id . 'max:MAX:%5.' . $float_precision . 'lf%s' . $this->units; + $rrd_optionsb[] = 'GPRINT:' . $id . ':AVERAGE:%5.' . $float_precision . "lf%s{$this->units}\\n"; + } + + array_push($rrd_options, ...$rrd_optionsb); + $rrd_options[] = 'HRULE:0#555555'; + + return $rrd_options; + } +} diff --git a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php new file mode 100644 index 00000000000..f8747f96cc1 --- /dev/null +++ b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php @@ -0,0 +1,251 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing\Builders; + +use App\Facades\LibrenmsConfig; +use LibreNMS\Data\Graphing\GraphParameters; +use LibreNMS\Data\Store\Rrd; + +class MultiSimplexSeparatedGraphBuilder +{ + private string $unitText = ''; + private string $units = ''; + private string $totalUnits = ''; + private string $colours = 'mixed'; + private ?float $scaleMin = null; + private ?float $scaleMax = null; + private ?float $divider = null; + private ?float $multiplier = null; + private bool $textOrig = false; + private bool $nototal = false; + private int $descrLen = 12; + + private array $datasets = []; + + public function unitText(string $unitText): self + { + $this->unitText = $unitText; + return $this; + } + + public function units(string $units): self + { + $this->units = $units; + return $this; + } + + public function totalUnits(string $totalUnits): self + { + $this->totalUnits = $totalUnits; + return $this; + } + + public function colours(string $colours): self + { + $this->colours = $colours; + return $this; + } + + public function scaleMin(float $scaleMin): self + { + $this->scaleMin = $scaleMin; + return $this; + } + + public function scaleMax(float $scaleMax): self + { + $this->scaleMax = $scaleMax; + return $this; + } + + public function divider(float $divider): self + { + $this->divider = $divider; + return $this; + } + + public function multiplier(float $multiplier): self + { + $this->multiplier = $multiplier; + return $this; + } + + public function textOrig(bool $textOrig = true): self + { + $this->textOrig = $textOrig; + return $this; + } + + public function noTotal(bool $noTotal = true): self + { + $this->nototal = $noTotal; + return $this; + } + + public function descrLen(int $descrLen): self + { + $this->descrLen = $descrLen; + return $this; + } + + public function addDataset( + string $filename, + string $ds, + string $description, + ?string $colour = null + ): self { + $this->datasets[] = [ + 'filename' => $filename, + 'ds' => $ds, + 'descr' => $description, + 'colour' => $colour, + ]; + return $this; + } + + public function build(array $vars): array + { + $graph_params = new GraphParameters($vars); + $float_precision = $graph_params->float_precision; + + if ($this->scaleMin !== null) { + $graph_params->scale_min = (int) $this->scaleMin; + } + if ($this->scaleMax !== null) { + $graph_params->scale_max = (int) $this->scaleMax; + } + + $previous = $graph_params->visible('previous'); + $prev_from = $graph_params->prev_from; + $from = $graph_params->from; + $period = $graph_params->period; + + $descr_len = $this->descrLen; + if ($this->nototal) { + $descr_len += 2; + } + + $unitlen = 10; + if ($this->nototal) { + $unitlen += 2; + } + + $unit_text = Rrd::fixedSafeDescr($this->unitText, $unitlen); + + $rrd_options = []; + $rrd_options[] = 'COMMENT:' . Rrd::fixedSafeDescr($this->unitText, $descr_len) . " Now Min Max Avg\l"; + + $seperatorX = ''; + $thingX = ''; + $plusX = ''; + $plusesX = ''; + $stack = ''; + + $colour_iter = 0; + foreach ($this->datasets as $i => $rrd) { + $colour = $rrd['colour']; + if ($colour === null) { + if (! LibrenmsConfig::get("graph_colours.{$this->colours}.{$colour_iter}")) { + $colour_iter = 0; + } + $colour = LibrenmsConfig::get("graph_colours.{$this->colours}.{$colour_iter}"); + $colour_iter++; + } + + $descr = Rrd::fixedSafeDescr($rrd['descr'], $descr_len); + $ds = $rrd['ds']; + $filename = $rrd['filename']; + + $rrd_options[] = 'DEF:' . $ds . $i . '=' . $filename . ':' . $ds . ':AVERAGE'; + $rrd_options[] = 'DEF:' . $ds . $i . 'min=' . $filename . ':' . $ds . ':MIN'; + $rrd_options[] = 'DEF:' . $ds . $i . 'max=' . $filename . ':' . $ds . ':MAX'; + + if ($previous) { + $rrd_options[] = 'DEF:' . $i . 'X=' . $filename . ':' . $ds . ':AVERAGE:start=' . $prev_from . ':end=' . $from; + $rrd_options[] = 'SHIFT:' . $i . "X:$period"; + $thingX .= $seperatorX . $i . 'X,UN,0,' . $i . 'X,IF'; + $plusesX .= $plusX; + $seperatorX = ','; + $plusX = ',+'; + } + + if (! $this->nototal) { + $rrd_options[] = 'VDEF:tot' . $ds . $i . '=' . $ds . $i . ',TOTAL'; + } + + if ($i > 0) { + $stack = ':STACK'; + } + + $g_defname = $ds; + if ($this->multiplier !== null) { + $g_defname = $ds . '_cdef'; + $rrd_options[] = 'CDEF:' . $g_defname . $i . '=' . $ds . $i . ',' . $this->multiplier . ',*'; + $rrd_options[] = 'CDEF:' . $g_defname . $i . 'min=' . $ds . $i . 'min,' . $this->multiplier . ',*'; + $rrd_options[] = 'CDEF:' . $g_defname . $i . 'max=' . $ds . $i . 'max,' . $this->multiplier . ',*'; + } elseif ($this->divider !== null) { + $g_defname = $ds . '_cdef'; + $rrd_options[] = 'CDEF:' . $g_defname . $i . '=' . $ds . $i . ',' . $this->divider . ',/'; + $rrd_options[] = 'CDEF:' . $g_defname . $i . 'min=' . $ds . $i . 'min,' . $this->divider . ',/'; + $rrd_options[] = 'CDEF:' . $g_defname . $i . 'max=' . $ds . $i . 'max,' . $this->divider . ',/'; + } + + if ($this->textOrig) { + $t_defname = $ds; + } else { + $t_defname = $g_defname; + } + + $rrd_options[] = 'AREA:' . $g_defname . $i . '#' . $colour . ':' . $descr . "$stack"; + + $rrd_options[] = 'GPRINT:' . $t_defname . $i . ':LAST:%5.' . $float_precision . 'lf%s'; + $rrd_options[] = 'GPRINT:' . $t_defname . $i . 'min:MIN:%5.' . $float_precision . 'lf%s'; + $rrd_options[] = 'GPRINT:' . $t_defname . $i . 'max:MAX:%5.' . $float_precision . 'lf%s'; + $rrd_options[] = 'GPRINT:' . $t_defname . $i . ':AVERAGE:%5.' . $float_precision . 'lf%s\\n'; + + if (! $this->nototal) { + $rrd_options[] = 'GPRINT:tot' . $ds . $i . ':%6.' . $float_precision . 'lf%s' . Rrd::safeDescr($this->totalUnits); + } + + $rrd_options[] = 'COMMENT:\\n'; + } + + if ($previous) { + if ($this->multiplier !== null) { + $rrd_options[] = 'CDEF:X=' . $thingX . $plusesX . ',' . $this->multiplier . ',*'; + } elseif ($this->divider !== null) { + $rrd_options[] = 'CDEF:X=' . $thingX . $plusesX . ',' . $this->divider . ',/'; + } else { + $rrd_options[] = 'CDEF:X=' . $thingX . $plusesX; + } + + $rrd_options[] = 'AREA:X#99999999:'; + $rrd_options[] = 'LINE1.25:X#666666:'; + } + + return $rrd_options; + } +} diff --git a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php new file mode 100644 index 00000000000..0c3239e7cf4 --- /dev/null +++ b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php @@ -0,0 +1,144 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2026 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing\Device; + +use App\Facades\DeviceCache; +use App\Facades\LibrenmsConfig; +use App\Facades\Rrd; +use App\Models\Device; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Gate; +use LibreNMS\Data\Graphing\AbstractGraph; +use LibreNMS\Data\Graphing\Builders\MultiLineGraphBuilder; +use LibreNMS\Data\Graphing\Builders\MultiSimplexSeparatedGraphBuilder; +use LibreNMS\Exceptions\RrdGraphException; +use LibreNMS\Util\Rewrite; + +class ProcessorGraph extends AbstractGraph +{ + public string $type = 'device'; + public string $subtype = 'processor'; + + private Device $device; + + + public function __construct(private readonly array $vars = []) + { + $this->device = DeviceCache::get($this->vars['device'] ?? null); + } + + public function authorize(): bool + { + return Gate::allows('view', $this->device); + } + + public function getGraphTitle(): string + { + return $this->device->display ?? ''; + } + + public function getRrdFiles(): array + { + $procs = DB::table('processors')->where('device_id', $this->device->device_id)->get(); + $files = []; + foreach ($procs as $proc) { + $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); + if (Rrd::checkRrdExists($rrd_filename)) { + $files[] = $rrd_filename; + } + } + return $files; + } + + public function definition(array $vars = []): array + { + $this->authorize(); + + $procs = DB::table('processors')->where('device_id', $this->device->device_id)->get(); + if ($procs->isEmpty()) { + throw new RrdGraphException('No Processors'); + } + + $rrd_list = []; + foreach ($procs as $proc) { + $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); + if (Rrd::checkRrdExists($rrd_filename)) { + $descr = Rewrite::shortHrDevice($proc->processor_descr); + $rrd_list[] = [ + 'filename' => $rrd_filename, + 'descr' => $descr, + 'ds' => 'usage', + ]; + } + } + + $vars = array_merge($this->vars, $vars); + + // Check if stacked processor graph is configured for this OS + if (LibrenmsConfig::getOsSetting($this->device->os, 'processor_stacked')) { + $builder = (new MultiSimplexSeparatedGraphBuilder()) + ->unitText('Load %') + ->units('%') + ->totalUnits('%') + ->colours('oranges') + ->scaleMin(0) + ->scaleMax(100) + ->divider(count($rrd_list)) + ->textOrig() + ->noTotal(); + + foreach ($rrd_list as $rrd) { + $builder->addDataset( + filename: $rrd['filename'], + ds: $rrd['ds'], + description: $rrd['descr'] + ); + } + + return $builder->build($vars); + } + + $builder = (new MultiLineGraphBuilder()) + ->unitText('Load %') + ->units('') + ->totalUnits('%') + ->colours('mixed') + ->scaleMin(0) + ->scaleMax(100) + ->noTotal(); + + foreach ($rrd_list as $rrd) { + $builder->addDataset( + filename: $rrd['filename'], + ds: $rrd['ds'], + description: $rrd['descr'], + area: true + ); + } + + return $builder->build($vars); + } +} diff --git a/LibreNMS/Data/Graphing/GraphFactory.php b/LibreNMS/Data/Graphing/GraphFactory.php index d2c1b77e06b..bab8ad240b2 100644 --- a/LibreNMS/Data/Graphing/GraphFactory.php +++ b/LibreNMS/Data/Graphing/GraphFactory.php @@ -39,8 +39,17 @@ public function graphFor(string $name, array $vars = []): GraphInterface throw new InvalidGraph; } + $type = $matches[1]; + $subtype = $matches[2]; + $vars['type'] ??= $name; - return new LegacyGraph($matches[1], $matches[2], $vars); // Only legacy for now + // Look for a modern class, e.g. LibreNMS\Data\Graphing\Device\ProcessorSeparateGraph + $className = "LibreNMS\\Data\\Graphing\\" . ucfirst($type) . "\\" . \Illuminate\Support\Str::studly($subtype) . "Graph"; + if (class_exists($className)) { + return app($className, ['vars' => $vars]); + } + + return new LegacyGraph($type, $subtype, $vars); } } diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index 19e919dda69..d8c72828df9 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -29,10 +29,9 @@ use App\Models\Device; use App\Models\Port; use LibreNMS\Exceptions\InvalidGraph; -use LibreNMS\Interfaces\Data\Graphing\GraphInterface; use function base_path; -class LegacyGraph implements GraphInterface +class LegacyGraph extends AbstractGraph { private readonly string $auth_file; private readonly string $graph_file; @@ -149,10 +148,6 @@ public function authorize(): bool return $this->authorized; } - public function validation(): array - { - return []; - } public function definition(array $vars = []): array { diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index f745c4e0920..f5e1f4ca16b 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -167,6 +167,11 @@ public static function getRrdOptions(array|string $vars, ?string &$rrd_filename if ($validator->fails()) { throw new ValidationException($validator); } + $vars = $validator->validated(); + } + + if ($graph instanceof \LibreNMS\Data\Graphing\AbstractGraph) { + $graph->fill($vars); } if (! $graph->authorize()) { diff --git a/LibreNMS/Util/Rewrite.php b/LibreNMS/Util/Rewrite.php index 0c3914806ba..d8a2ef60568 100644 --- a/LibreNMS/Util/Rewrite.php +++ b/LibreNMS/Util/Rewrite.php @@ -405,4 +405,21 @@ public static function celsiusToFahrenheit(float $celsius): float { return round($celsius * 1.8 + 32, 2); } + + public static function shortHrDevice(string $descr): string + { + $rewrite_hrDevice = [ + 'GenuineIntel:' => '', + 'AuthenticAMD:' => '', + 'Intel(R)' => '', + 'CPU' => '', + '(R)' => '', + ' ' => ' ', + ]; + + $descr = str_replace(array_keys($rewrite_hrDevice), array_values($rewrite_hrDevice), $descr); + $descr = preg_replace('/\ +/', ' ', $descr); + + return trim($descr); + } } From c8e43e1900d8901c5ff82f10e8f9e1a5d2152c41 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Tue, 30 Jun 2026 18:39:20 -0500 Subject: [PATCH 04/16] Refactor and cleanup the processor graph --- .../Data/Graphing/Device/ProcessorGraph.php | 96 ++++++++----------- LibreNMS/Data/Graphing/LegacyGraph.php | 2 +- LibreNMS/Util/Rewrite.php | 17 ---- app/Models/Processor.php | 6 +- 4 files changed, 43 insertions(+), 78 deletions(-) diff --git a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php index 0c3239e7cf4..39d46daaa1d 100644 --- a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php +++ b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php @@ -29,13 +29,11 @@ use App\Facades\LibrenmsConfig; use App\Facades\Rrd; use App\Models\Device; -use Illuminate\Support\Facades\DB; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Gate; use LibreNMS\Data\Graphing\AbstractGraph; use LibreNMS\Data\Graphing\Builders\MultiLineGraphBuilder; use LibreNMS\Data\Graphing\Builders\MultiSimplexSeparatedGraphBuilder; -use LibreNMS\Exceptions\RrdGraphException; -use LibreNMS\Util\Rewrite; class ProcessorGraph extends AbstractGraph { @@ -43,16 +41,22 @@ class ProcessorGraph extends AbstractGraph public string $subtype = 'processor'; private Device $device; + private Collection $processors; - - public function __construct(private readonly array $vars = []) - { + public function __construct( + private readonly array $vars = [], + ) { $this->device = DeviceCache::get($this->vars['device'] ?? null); + $this->processors = $this->device->exists ? $this->device->processors : new Collection; } public function authorize(): bool { - return Gate::allows('view', $this->device); + if ($processor = $this->processors->first()) { + return Gate::allows('view', $processor); + } + + return $this->device->exists && Gate::allows('view', $this->device); } public function getGraphTitle(): string @@ -62,42 +66,30 @@ public function getGraphTitle(): string public function getRrdFiles(): array { - $procs = DB::table('processors')->where('device_id', $this->device->device_id)->get(); $files = []; - foreach ($procs as $proc) { - $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); - if (Rrd::checkRrdExists($rrd_filename)) { - $files[] = $rrd_filename; - } + foreach ($this->processors as $proc) { + $files[] = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); } return $files; } public function definition(array $vars = []): array { - $this->authorize(); - - $procs = DB::table('processors')->where('device_id', $this->device->device_id)->get(); - if ($procs->isEmpty()) { - throw new RrdGraphException('No Processors'); + if ($this->processors->isEmpty()) { + throw new \LibreNMS\Exceptions\RrdGraphException('No Processors'); } - $rrd_list = []; - foreach ($procs as $proc) { + $vars = array_merge($this->vars, $vars); + + // Count how many files actually exist on disk for the layout + $rrd_count = 0; + foreach ($this->processors as $proc) { $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); if (Rrd::checkRrdExists($rrd_filename)) { - $descr = Rewrite::shortHrDevice($proc->processor_descr); - $rrd_list[] = [ - 'filename' => $rrd_filename, - 'descr' => $descr, - 'ds' => 'usage', - ]; + $rrd_count++; } } - $vars = array_merge($this->vars, $vars); - - // Check if stacked processor graph is configured for this OS if (LibrenmsConfig::getOsSetting($this->device->os, 'processor_stacked')) { $builder = (new MultiSimplexSeparatedGraphBuilder()) ->unitText('Load %') @@ -106,37 +98,29 @@ public function definition(array $vars = []): array ->colours('oranges') ->scaleMin(0) ->scaleMax(100) - ->divider(count($rrd_list)) + ->divider(max(1, $rrd_count)) ->textOrig() ->noTotal(); - - foreach ($rrd_list as $rrd) { - $builder->addDataset( - filename: $rrd['filename'], - ds: $rrd['ds'], - description: $rrd['descr'] - ); - } - - return $builder->build($vars); + } else { + $builder = (new MultiLineGraphBuilder()) + ->unitText('Load %') + ->units('') + ->totalUnits('%') + ->colours('mixed') + ->scaleMin(0) + ->scaleMax(100) + ->noTotal(); } - $builder = (new MultiLineGraphBuilder()) - ->unitText('Load %') - ->units('') - ->totalUnits('%') - ->colours('mixed') - ->scaleMin(0) - ->scaleMax(100) - ->noTotal(); - - foreach ($rrd_list as $rrd) { - $builder->addDataset( - filename: $rrd['filename'], - ds: $rrd['ds'], - description: $rrd['descr'], - area: true - ); + foreach ($this->processors as $proc) { + $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); + if (Rrd::checkRrdExists($rrd_filename)) { + if ($builder instanceof MultiLineGraphBuilder) { + $builder->addDataset($rrd_filename, 'usage', $proc->getFormattedDescription(), area: true); + } else { + $builder->addDataset($rrd_filename, 'usage', $proc->getFormattedDescription()); + } + } } return $builder->build($vars); diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index d8c72828df9..66877465a2c 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -79,7 +79,7 @@ private function load(array $vars = []): void include_once base_path('includes/rewrites.php'); if (! $this->device && isset($this->vars['device'])) { - $this->device = DeviceCache::get(is_numeric($this->vars['device']) ? $this->vars['device'] : getidbyname($this->vars['device'])); + $this->device = DeviceCache::get($this->vars['device']); } if ($this->device) { diff --git a/LibreNMS/Util/Rewrite.php b/LibreNMS/Util/Rewrite.php index d8a2ef60568..0c3914806ba 100644 --- a/LibreNMS/Util/Rewrite.php +++ b/LibreNMS/Util/Rewrite.php @@ -405,21 +405,4 @@ public static function celsiusToFahrenheit(float $celsius): float { return round($celsius * 1.8 + 32, 2); } - - public static function shortHrDevice(string $descr): string - { - $rewrite_hrDevice = [ - 'GenuineIntel:' => '', - 'AuthenticAMD:' => '', - 'Intel(R)' => '', - 'CPU' => '', - '(R)' => '', - ' ' => ' ', - ]; - - $descr = str_replace(array_keys($rewrite_hrDevice), array_values($rewrite_hrDevice), $descr); - $descr = preg_replace('/\ +/', ' ', $descr); - - return trim($descr); - } } diff --git a/app/Models/Processor.php b/app/Models/Processor.php index 6c539cdd3f8..26d9502d8eb 100644 --- a/app/Models/Processor.php +++ b/app/Models/Processor.php @@ -18,7 +18,7 @@ class Processor extends DeviceRelatedModel * * @return string */ - public function getFormattedDescription() + public function getFormattedDescription(): string { $bad_descr = [ 'GenuineIntel:', @@ -32,8 +32,6 @@ public function getFormattedDescription() $descr = str_replace($bad_descr, '', $this->processor_descr); // reduce extra spaces - $descr = str_replace(' ', ' ', $descr); - - return $descr; + return str_replace(' ', ' ', $descr); } } From ba53e756c5c87b4527b92bdb8ff997517c206681 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Tue, 30 Jun 2026 18:49:24 -0500 Subject: [PATCH 05/16] refactor more --- .../Data/Graphing/Device/ProcessorGraph.php | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php index 39d46daaa1d..78264b06f5a 100644 --- a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php +++ b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php @@ -81,15 +81,20 @@ public function definition(array $vars = []): array $vars = array_merge($this->vars, $vars); - // Count how many files actually exist on disk for the layout - $rrd_count = 0; + // Filter valid datasets and run checkRrdExists exactly once per processor + $valid_datasets = []; foreach ($this->processors as $proc) { $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); if (Rrd::checkRrdExists($rrd_filename)) { - $rrd_count++; + $valid_datasets[] = [ + 'filename' => $rrd_filename, + 'descr' => $proc->getFormattedDescription(), + ]; } } + $rrd_count = count($valid_datasets); + if (LibrenmsConfig::getOsSetting($this->device->os, 'processor_stacked')) { $builder = (new MultiSimplexSeparatedGraphBuilder()) ->unitText('Load %') @@ -101,26 +106,25 @@ public function definition(array $vars = []): array ->divider(max(1, $rrd_count)) ->textOrig() ->noTotal(); - } else { - $builder = (new MultiLineGraphBuilder()) - ->unitText('Load %') - ->units('') - ->totalUnits('%') - ->colours('mixed') - ->scaleMin(0) - ->scaleMax(100) - ->noTotal(); - } - foreach ($this->processors as $proc) { - $rrd_filename = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); - if (Rrd::checkRrdExists($rrd_filename)) { - if ($builder instanceof MultiLineGraphBuilder) { - $builder->addDataset($rrd_filename, 'usage', $proc->getFormattedDescription(), area: true); - } else { - $builder->addDataset($rrd_filename, 'usage', $proc->getFormattedDescription()); - } + foreach ($valid_datasets as $dataset) { + $builder->addDataset($dataset['filename'], 'usage', $dataset['descr']); } + + return $builder->build($vars); + } + + $builder = (new MultiLineGraphBuilder()) + ->unitText('Load %') + ->units('') + ->totalUnits('%') + ->colours('mixed') + ->scaleMin(0) + ->scaleMax(100) + ->noTotal(); + + foreach ($valid_datasets as $dataset) { + $builder->addDataset($dataset['filename'], 'usage', $dataset['descr'], area: true); } return $builder->build($vars); From 17e795a60d8afb9aa230c641831496a5370a0b30 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Tue, 30 Jun 2026 18:56:28 -0500 Subject: [PATCH 06/16] silly --- .../Data/Graphing/Builders/MultiLineGraphBuilder.php | 3 +-- .../Builders/MultiSimplexSeparatedGraphBuilder.php | 3 +-- LibreNMS/Data/Graphing/Device/ProcessorGraph.php | 9 ++++----- LibreNMS/Data/Graphing/LegacyGraph.php | 8 ++++---- LibreNMS/Interfaces/Data/Graphing/GraphInterface.php | 4 +++- LibreNMS/Util/Graph.php | 3 ++- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php index f5cdebefbe6..a66f3879914 100644 --- a/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php +++ b/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php @@ -111,9 +111,8 @@ public function addDataset( return $this; } - public function build(array $vars): array + public function build(GraphParameters $graph_params): array { - $graph_params = new GraphParameters($vars); $float_precision = $graph_params->float_precision; if ($this->scaleMin !== null) { diff --git a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php index f8747f96cc1..fa088eac262 100644 --- a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php +++ b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php @@ -126,9 +126,8 @@ public function addDataset( return $this; } - public function build(array $vars): array + public function build(GraphParameters $graph_params): array { - $graph_params = new GraphParameters($vars); $float_precision = $graph_params->float_precision; if ($this->scaleMin !== null) { diff --git a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php index 78264b06f5a..dc3754d1805 100644 --- a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php +++ b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php @@ -34,6 +34,7 @@ use LibreNMS\Data\Graphing\AbstractGraph; use LibreNMS\Data\Graphing\Builders\MultiLineGraphBuilder; use LibreNMS\Data\Graphing\Builders\MultiSimplexSeparatedGraphBuilder; +use LibreNMS\Data\Graphing\GraphParameters; class ProcessorGraph extends AbstractGraph { @@ -73,14 +74,12 @@ public function getRrdFiles(): array return $files; } - public function definition(array $vars = []): array + public function definition(GraphParameters $graph_params): array { if ($this->processors->isEmpty()) { throw new \LibreNMS\Exceptions\RrdGraphException('No Processors'); } - $vars = array_merge($this->vars, $vars); - // Filter valid datasets and run checkRrdExists exactly once per processor $valid_datasets = []; foreach ($this->processors as $proc) { @@ -111,7 +110,7 @@ public function definition(array $vars = []): array $builder->addDataset($dataset['filename'], 'usage', $dataset['descr']); } - return $builder->build($vars); + return $builder->build($graph_params); } $builder = (new MultiLineGraphBuilder()) @@ -127,6 +126,6 @@ public function definition(array $vars = []): array $builder->addDataset($dataset['filename'], 'usage', $dataset['descr'], area: true); } - return $builder->build($vars); + return $builder->build($graph_params); } } diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index 66877465a2c..cee665eaaef 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -67,7 +67,7 @@ public function __construct( private function load(array $vars = []): void { - if ($this->loaded && empty($vars)) { + if ($this->loaded) { return; } @@ -107,7 +107,7 @@ private function load(array $vars = []): void return; } - $graph_params = new GraphParameters($vars); + $graph_params = app()->bound(GraphParameters::class) ? app(GraphParameters::class) : new GraphParameters($this->vars); $type = $graph_params->type; $subtype = $graph_params->subtype; $height = $graph_params->height; @@ -149,9 +149,9 @@ public function authorize(): bool } - public function definition(array $vars = []): array + public function definition(GraphParameters $graph_params): array { - $this->load($vars); + $this->load($graph_params->all()); return $this->rrdOptions; } diff --git a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php index 266c4cc70b4..eb73dec0861 100644 --- a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php +++ b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php @@ -25,11 +25,13 @@ namespace LibreNMS\Interfaces\Data\Graphing; +use LibreNMS\Data\Graphing\GraphParameters; + interface GraphInterface { public function authorize(): bool; public function validation(): array; - public function definition(array $vars = []): array; + public function definition(GraphParameters $graph_params): array; public function getPageTitle(): string; public function getGraphTitle(): string; public function getRrdFiles(): array; diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index f5e1f4ca16b..5fe44f561e8 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -110,6 +110,7 @@ public static function get(array|string $vars): GraphImage } $graph_params = new GraphParameters($vars); + app()->instance(GraphParameters::class, $graph_params); $name = $graph_params->type . '_' . $graph_params->subtype; /** @var GraphFactory $factory */ @@ -178,7 +179,7 @@ public static function getRrdOptions(array|string $vars, ?string &$rrd_filename throw new RrdGraphException('No Authorization', 'No Auth', $graph_params->width, $graph_params->height); } - $rrd_options = $graph->definition($vars); + $rrd_options = $graph->definition($graph_params); if (empty($rrd_options)) { throw new RrdGraphException('Graph Definition Error', 'Def Error', $graph_params->width, $graph_params->height); From 7a22ffc86784310fa543be3f00455d3225541897 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 1 Jul 2026 09:24:31 -0500 Subject: [PATCH 07/16] some cleanups --- .../Data/Graphing/Builders/MultiLineGraphBuilder.php | 7 ------- .../Builders/MultiSimplexSeparatedGraphBuilder.php | 7 ------- LibreNMS/Data/Graphing/Device/ProcessorGraph.php | 2 -- LibreNMS/Data/Graphing/LegacyGraph.php | 9 ++++----- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php index a66f3879914..f1d8a6ea127 100644 --- a/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php +++ b/LibreNMS/Data/Graphing/Builders/MultiLineGraphBuilder.php @@ -33,7 +33,6 @@ class MultiLineGraphBuilder { private string $unitText = ''; private string $units = ''; - private string $totalUnits = '%'; private string $colours = 'mixed'; private ?float $scaleMin = null; private ?float $scaleMax = null; @@ -54,12 +53,6 @@ public function units(string $units): self return $this; } - public function totalUnits(string $totalUnits): self - { - $this->totalUnits = $totalUnits; - return $this; - } - public function colours(string $colours): self { $this->colours = $colours; diff --git a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php index fa088eac262..66740948a57 100644 --- a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php +++ b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php @@ -32,7 +32,6 @@ class MultiSimplexSeparatedGraphBuilder { private string $unitText = ''; - private string $units = ''; private string $totalUnits = ''; private string $colours = 'mixed'; private ?float $scaleMin = null; @@ -51,12 +50,6 @@ public function unitText(string $unitText): self return $this; } - public function units(string $units): self - { - $this->units = $units; - return $this; - } - public function totalUnits(string $totalUnits): self { $this->totalUnits = $totalUnits; diff --git a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php index dc3754d1805..63f0d03fefd 100644 --- a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php +++ b/LibreNMS/Data/Graphing/Device/ProcessorGraph.php @@ -97,7 +97,6 @@ public function definition(GraphParameters $graph_params): array if (LibrenmsConfig::getOsSetting($this->device->os, 'processor_stacked')) { $builder = (new MultiSimplexSeparatedGraphBuilder()) ->unitText('Load %') - ->units('%') ->totalUnits('%') ->colours('oranges') ->scaleMin(0) @@ -116,7 +115,6 @@ public function definition(GraphParameters $graph_params): array $builder = (new MultiLineGraphBuilder()) ->unitText('Load %') ->units('') - ->totalUnits('%') ->colours('mixed') ->scaleMin(0) ->scaleMax(100) diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index cee665eaaef..b48ec0d1d1e 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -46,12 +46,13 @@ class LegacyGraph extends AbstractGraph private ?Port $port = null; /** + * @param array $vars * @throws InvalidGraph */ public function __construct( public readonly string $type, public readonly string $subtype, - private array $vars = [], + private readonly array $vars = [], ) { $this->auth_file = base_path("includes/html/graphs/$this->type/auth.inc.php"); $graph_file = base_path("includes/html/graphs/$this->type/$this->subtype.inc.php"); @@ -65,14 +66,12 @@ public function __construct( } } - private function load(array $vars = []): void + private function load(): void { if ($this->loaded) { return; } - $this->vars = array_merge($this->vars, $vars); - include_once base_path('includes/common.php'); include_once base_path('includes/html/functions.inc.php'); include_once base_path('includes/dbFacile.php'); @@ -151,7 +150,7 @@ public function authorize(): bool public function definition(GraphParameters $graph_params): array { - $this->load($graph_params->all()); + $this->load(); return $this->rrdOptions; } From da43d27fb25c611120dc960a4b7cb2100ddb0340 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 1 Jul 2026 09:27:49 -0500 Subject: [PATCH 08/16] Move graph definitions to keep it clean --- LibreNMS/Data/Graphing/GraphFactory.php | 3 ++- LibreNMS/{Data/Graphing => Graphs}/Device/ProcessorGraph.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename LibreNMS/{Data/Graphing => Graphs}/Device/ProcessorGraph.php (99%) diff --git a/LibreNMS/Data/Graphing/GraphFactory.php b/LibreNMS/Data/Graphing/GraphFactory.php index bab8ad240b2..16d24b311f4 100644 --- a/LibreNMS/Data/Graphing/GraphFactory.php +++ b/LibreNMS/Data/Graphing/GraphFactory.php @@ -25,6 +25,7 @@ namespace LibreNMS\Data\Graphing; +use Illuminate\Support\Str; use LibreNMS\Exceptions\InvalidGraph; use LibreNMS\Interfaces\Data\Graphing\GraphInterface; @@ -45,7 +46,7 @@ public function graphFor(string $name, array $vars = []): GraphInterface $vars['type'] ??= $name; // Look for a modern class, e.g. LibreNMS\Data\Graphing\Device\ProcessorSeparateGraph - $className = "LibreNMS\\Data\\Graphing\\" . ucfirst($type) . "\\" . \Illuminate\Support\Str::studly($subtype) . "Graph"; + $className = "LibreNMS\\Graphs\\" . ucfirst($type) . "\\" . Str::studly($subtype) . "Graph"; if (class_exists($className)) { return app($className, ['vars' => $vars]); } diff --git a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php b/LibreNMS/Graphs/Device/ProcessorGraph.php similarity index 99% rename from LibreNMS/Data/Graphing/Device/ProcessorGraph.php rename to LibreNMS/Graphs/Device/ProcessorGraph.php index 63f0d03fefd..9eea2b87207 100644 --- a/LibreNMS/Data/Graphing/Device/ProcessorGraph.php +++ b/LibreNMS/Graphs/Device/ProcessorGraph.php @@ -23,7 +23,7 @@ * @author Tony Murray */ -namespace LibreNMS\Data\Graphing\Device; +namespace LibreNMS\Graphs\Device; use App\Facades\DeviceCache; use App\Facades\LibrenmsConfig; From 99a7857e496eb5f8f780f661b0841922983d707b Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 1 Jul 2026 10:19:20 -0500 Subject: [PATCH 09/16] Style fixes --- LibreNMS/Data/Graphing/AbstractGraph.php | 2 ++ .../Data/Graphing/Builders/MultiLineGraphBuilder.php | 9 +++++++++ .../Builders/MultiSimplexSeparatedGraphBuilder.php | 12 ++++++++++++ LibreNMS/Data/Graphing/GraphFactory.php | 3 ++- LibreNMS/Data/Graphing/LegacyGraph.php | 8 +++++++- LibreNMS/Exceptions/InvalidGraph.php | 9 +++++---- LibreNMS/Graphs/Device/ProcessorGraph.php | 2 ++ LibreNMS/Interfaces/Data/Graphing/GraphInterface.php | 6 ++++++ app/Console/Commands/GraphDetail.php | 9 ++++----- 9 files changed, 49 insertions(+), 11 deletions(-) diff --git a/LibreNMS/Data/Graphing/AbstractGraph.php b/LibreNMS/Data/Graphing/AbstractGraph.php index e4a9fe9f246..1b0392d9e46 100644 --- a/LibreNMS/Data/Graphing/AbstractGraph.php +++ b/LibreNMS/Data/Graphing/AbstractGraph.php @@ -1,4 +1,5 @@ unitText = $unitText; + return $this; } public function units(string $units): self { $this->units = $units; + return $this; } public function colours(string $colours): self { $this->colours = $colours; + return $this; } public function scaleMin(float $scaleMin): self { $this->scaleMin = $scaleMin; + return $this; } public function scaleMax(float $scaleMax): self { $this->scaleMax = $scaleMax; + return $this; } public function noTotal(bool $noTotal = true): self { $this->nototal = $noTotal; + return $this; } public function descrLen(int $descrLen): self { $this->descrLen = $descrLen; + return $this; } @@ -101,6 +109,7 @@ public function addDataset( 'areacolour' => $areaColour, 'invert' => $invert, ]; + return $this; } diff --git a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php index 66740948a57..22f3ce50b50 100644 --- a/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php +++ b/LibreNMS/Data/Graphing/Builders/MultiSimplexSeparatedGraphBuilder.php @@ -1,4 +1,5 @@ unitText = $unitText; + return $this; } public function totalUnits(string $totalUnits): self { $this->totalUnits = $totalUnits; + return $this; } public function colours(string $colours): self { $this->colours = $colours; + return $this; } public function scaleMin(float $scaleMin): self { $this->scaleMin = $scaleMin; + return $this; } public function scaleMax(float $scaleMax): self { $this->scaleMax = $scaleMax; + return $this; } public function divider(float $divider): self { $this->divider = $divider; + return $this; } public function multiplier(float $multiplier): self { $this->multiplier = $multiplier; + return $this; } public function textOrig(bool $textOrig = true): self { $this->textOrig = $textOrig; + return $this; } public function noTotal(bool $noTotal = true): self { $this->nototal = $noTotal; + return $this; } public function descrLen(int $descrLen): self { $this->descrLen = $descrLen; + return $this; } @@ -116,6 +127,7 @@ public function addDataset( 'descr' => $description, 'colour' => $colour, ]; + return $this; } diff --git a/LibreNMS/Data/Graphing/GraphFactory.php b/LibreNMS/Data/Graphing/GraphFactory.php index 16d24b311f4..397be357415 100644 --- a/LibreNMS/Data/Graphing/GraphFactory.php +++ b/LibreNMS/Data/Graphing/GraphFactory.php @@ -1,4 +1,5 @@ $vars]); } diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index b48ec0d1d1e..d8b8ff31de1 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -1,4 +1,5 @@ $vars + * * @throws InvalidGraph */ public function __construct( @@ -103,6 +105,7 @@ private function load(): void if (! $auth) { $this->loaded = true; + return; } @@ -144,19 +147,21 @@ private function load(): void public function authorize(): bool { $this->load(); + return $this->authorized; } - public function definition(GraphParameters $graph_params): array { $this->load(); + return $this->rrdOptions; } public function getPageTitle(): string { $this->load(); + return $this->pageTitle ?? $this->getGraphTitle(); } @@ -178,6 +183,7 @@ public function getGraphTitle(): string public function getRrdFiles(): array { $this->load(); + return $this->rrdFiles; } } diff --git a/LibreNMS/Exceptions/InvalidGraph.php b/LibreNMS/Exceptions/InvalidGraph.php index 6bb3ff163fa..99bde5b5423 100644 --- a/LibreNMS/Exceptions/InvalidGraph.php +++ b/LibreNMS/Exceptions/InvalidGraph.php @@ -1,4 +1,5 @@ processors as $proc) { $files[] = Rrd::name($this->device->hostname, ['processor', $proc->processor_type, $proc->processor_index]); } + return $files; } diff --git a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php index eb73dec0861..f4a0540edea 100644 --- a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php +++ b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php @@ -1,4 +1,5 @@ line("Type: $graph->type"); $this->line("Subtype: $graph->subtype"); $this->line('Authorized: ' . ($graph->authorize() ? 'true' : 'false')); - $this->line("Graph Title: " . $graph->getGraphTitle()); - $this->line("Page Title: " . substr($graph->getPageTitle(), 0, 80)); - $this->line("Rrd Files: " . implode(', ', array_map(fn($f) => basename($f), $graph->getRrdFiles()))); - $this->line("Definition: " . substr(implode(' ', $graph->definition()), 80)); - + $this->line('Graph Title: ' . $graph->getGraphTitle()); + $this->line('Page Title: ' . substr($graph->getPageTitle(), 0, 80)); + $this->line('Rrd Files: ' . implode(', ', array_map(fn ($f) => basename($f), $graph->getRrdFiles()))); + $this->line('Definition: ' . substr(implode(' ', $graph->definition()), 80)); return 0; } From 1ed555e8c3ac066d2145bdbc714909e3cf37b3ff Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 1 Jul 2026 10:21:44 -0500 Subject: [PATCH 10/16] Remove incorrect use statement --- LibreNMS/Data/Graphing/LegacyGraph.php | 1 - 1 file changed, 1 deletion(-) diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index d8b8ff31de1..987eff834a6 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -30,7 +30,6 @@ use App\Models\Device; use App\Models\Port; use LibreNMS\Exceptions\InvalidGraph; -use function base_path; class LegacyGraph extends AbstractGraph { From e01e33d9c494fe63ebbd5781abb0cec891ea6b98 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 1 Jul 2026 23:04:25 -0500 Subject: [PATCH 11/16] port netstat-ip graph --- LibreNMS/Data/Graphing/LegacyGraph.php | 2 +- LibreNMS/Graphs/Device/NetstatIpGraph.php | 55 +++++++++++++++++++ LibreNMS/Graphs/Device/ProcessorGraph.php | 2 +- .../Data/Graphing/GraphInterface.php | 2 +- LibreNMS/Util/Graph.php | 2 +- app/Console/Commands/GraphDetail.php | 2 +- 6 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 LibreNMS/Graphs/Device/NetstatIpGraph.php diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index 987eff834a6..76ee10f0b15 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -150,7 +150,7 @@ public function authorize(): bool return $this->authorized; } - public function definition(GraphParameters $graph_params): array + public function rrdDefinition(GraphParameters $graph_params): array { $this->load(); diff --git a/LibreNMS/Graphs/Device/NetstatIpGraph.php b/LibreNMS/Graphs/Device/NetstatIpGraph.php new file mode 100644 index 00000000000..e0e38931f2e --- /dev/null +++ b/LibreNMS/Graphs/Device/NetstatIpGraph.php @@ -0,0 +1,55 @@ +device = DeviceCache::get($this->vars['device'] ?? null); + } + + public function authorize(): bool + { + return Gate::allows('view', $this->device); + } + + public function rrdDefinition(GraphParameters $graph_params): array + { + $rrd_file = Rrd::name($this->device->hostname, 'netstats-ip'); + + return (new MultiLineGraphBuilder()) + ->scaleMin(0) + ->noTotal() + ->colours('mixed') + ->addDataset($rrd_file, 'ipForwDatagrams', 'Fwd Datagrams') + ->addDataset($rrd_file, 'ipInDelivers', 'In Delivers') + ->addDataset($rrd_file, 'ipInReceives', 'In Receives') + ->addDataset($rrd_file, 'ipOutRequests', 'Out Requests', invert: true) + ->addDataset($rrd_file, 'ipInDiscards', 'In Discards') + ->addDataset($rrd_file, 'ipOutDiscards', 'Out Discards', invert: true) + ->addDataset($rrd_file, 'ipOutNoRoutes', 'Out No Routes', invert: true) + ->build($graph_params); + } + + public function getGraphTitle(): string + { + return $this->device->display . ' :: IP NetStats'; + } + + public function getRrdFiles(): array + { + return [Rrd::name($this->device->hostname, 'netstats-ip')]; + } +} diff --git a/LibreNMS/Graphs/Device/ProcessorGraph.php b/LibreNMS/Graphs/Device/ProcessorGraph.php index 256b3684695..5888f5f9453 100644 --- a/LibreNMS/Graphs/Device/ProcessorGraph.php +++ b/LibreNMS/Graphs/Device/ProcessorGraph.php @@ -76,7 +76,7 @@ public function getRrdFiles(): array return $files; } - public function definition(GraphParameters $graph_params): array + public function rrdDefinition(GraphParameters $graph_params): array { if ($this->processors->isEmpty()) { throw new \LibreNMS\Exceptions\RrdGraphException('No Processors'); diff --git a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php index f4a0540edea..d7e08998a52 100644 --- a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php +++ b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php @@ -34,7 +34,7 @@ public function authorize(): bool; public function validation(): array; - public function definition(GraphParameters $graph_params): array; + public function rrdDefinition(GraphParameters $graph_params): array; public function getPageTitle(): string; diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index 5fe44f561e8..ea22b1f0792 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -179,7 +179,7 @@ public static function getRrdOptions(array|string $vars, ?string &$rrd_filename throw new RrdGraphException('No Authorization', 'No Auth', $graph_params->width, $graph_params->height); } - $rrd_options = $graph->definition($graph_params); + $rrd_options = $graph->rrdDefinition($graph_params); if (empty($rrd_options)) { throw new RrdGraphException('Graph Definition Error', 'Def Error', $graph_params->width, $graph_params->height); diff --git a/app/Console/Commands/GraphDetail.php b/app/Console/Commands/GraphDetail.php index d5af96d51ec..2e8f18fb4b6 100644 --- a/app/Console/Commands/GraphDetail.php +++ b/app/Console/Commands/GraphDetail.php @@ -27,7 +27,7 @@ public function handle(GraphFactory $graphs): int $this->line('Graph Title: ' . $graph->getGraphTitle()); $this->line('Page Title: ' . substr($graph->getPageTitle(), 0, 80)); $this->line('Rrd Files: ' . implode(', ', array_map(fn ($f) => basename($f), $graph->getRrdFiles()))); - $this->line('Definition: ' . substr(implode(' ', $graph->definition()), 80)); + $this->line('Definition: ' . substr(implode(' ', $graph->rrdDefinition()), 80)); return 0; } From bba70846fa3a00b1d8f24912f60772799c4f33f2 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Thu, 2 Jul 2026 00:52:31 -0500 Subject: [PATCH 12/16] clean ups --- LibreNMS/Data/Graphing/AbstractGraph.php | 54 +++++++++++----- LibreNMS/Data/Graphing/GraphFactory.php | 18 +++--- LibreNMS/Data/Graphing/GraphParameters.php | 4 +- LibreNMS/Data/Graphing/LegacyGraph.php | 38 +++++------- LibreNMS/Graphs/Device/NetstatIpGraph.php | 15 +---- LibreNMS/Graphs/Device/ProcessorGraph.php | 18 +++--- .../Data/Graphing/GraphInterface.php | 4 +- LibreNMS/Util/Graph.php | 62 ++++++------------- 8 files changed, 93 insertions(+), 120 deletions(-) diff --git a/LibreNMS/Data/Graphing/AbstractGraph.php b/LibreNMS/Data/Graphing/AbstractGraph.php index 1b0392d9e46..c8aaccca112 100644 --- a/LibreNMS/Data/Graphing/AbstractGraph.php +++ b/LibreNMS/Data/Graphing/AbstractGraph.php @@ -26,13 +26,48 @@ namespace LibreNMS\Data\Graphing; +use App\Facades\DeviceCache; +use App\Facades\PortCache; +use App\Models\Device; +use App\Models\Port; use LibreNMS\Interfaces\Data\Graphing\GraphInterface; abstract class AbstractGraph implements GraphInterface { + protected Device $device; + protected ?Port $port; + + public function __construct( + protected readonly GraphParameters $params, + protected readonly array $vars = [], + ) + { + $device_id = $this->vars['device'] ?? ($this->params->type == 'device' ? ($this->vars['id'] ?? null) : null); + $this->device = DeviceCache::get($device_id); + + $port_id = $this->vars['port'] ?? ($this->params->type == 'port' ? ($this->vars['id'] ?? null) : null); + $this->port = PortCache::get($port_id); + } + + public function getParams(): GraphParameters + { + return $this->params; + } + public function validation(): array { - return []; + return [ + 'type' => ['required', 'string', 'regex:/^[a-z][a-z0-9]*_[a-zA-Z0-9_]+$/'], + 'id' => ['nullable', 'integer'], + 'from' => ['nullable', 'regex:/^(-?\d+|-?\d+[smhdwMy]|now|end)$/'], + 'to' => ['nullable', 'regex:/^(-?\d+|-?\d+[smhdwMy]|now|end)$/'], + 'width' => ['nullable', 'integer', 'min:10', 'max:10000'], + 'height' => ['nullable', 'integer', 'min:10', 'max:8000'], + 'legend' => ['nullable', 'in:yes,no,0,1'], + 'bg' => ['nullable', 'regex:/^[0-9A-Fa-f]{6}$/'], + 'title' => ['nullable', 'string', 'max:255'], + 'output' => ['nullable', 'in:png,svg,json'], + ]; } public function getPageTitle(): string @@ -40,21 +75,8 @@ public function getPageTitle(): string return $this->getGraphTitle(); } - /** - * Bind validated parameters directly to class properties - */ - public function fill(array $vars): self + protected function init(): void { - $reflector = new \ReflectionClass($this); - foreach ($vars as $key => $value) { - if ($reflector->hasProperty($key)) { - $property = $reflector->getProperty($key); - if ($property->isPublic() && ! $property->isReadOnly()) { - $this->$key = $value; - } - } - } - - return $this; + // for child init } } diff --git a/LibreNMS/Data/Graphing/GraphFactory.php b/LibreNMS/Data/Graphing/GraphFactory.php index 397be357415..d4215a9c51f 100644 --- a/LibreNMS/Data/Graphing/GraphFactory.php +++ b/LibreNMS/Data/Graphing/GraphFactory.php @@ -37,21 +37,19 @@ class GraphFactory */ public function graphFor(string $name, array $vars = []): GraphInterface { - if (! preg_match('/([a-z]+)_([a-zA-Z0-9_]+)/', $name, $matches)) { + $vars['type'] ??= $name; + $params = new GraphParameters($vars); + + if (empty($params->type) || empty($params->subtype)) { throw new InvalidGraph; } - $type = $matches[1]; - $subtype = $matches[2]; - - $vars['type'] ??= $name; - - // Look for a modern class, e.g. LibreNMS\Data\Graphing\Device\ProcessorSeparateGraph - $className = 'LibreNMS\\Graphs\\' . ucfirst($type) . '\\' . Str::studly($subtype) . 'Graph'; + // Look for a modern class, e.g. LibreNMS\Graphs\Device\ProcessorGraph + $className = 'LibreNMS\\Graphs\\' . ucfirst($params->type) . '\\' . Str::studly($params->subtype) . 'Graph'; if (class_exists($className)) { - return app($className, ['vars' => $vars]); + return app($className, ['vars' => $vars, 'params' => $params]); } - return new LegacyGraph($type, $subtype, $vars); + return new LegacyGraph($params, $vars); } } diff --git a/LibreNMS/Data/Graphing/GraphParameters.php b/LibreNMS/Data/Graphing/GraphParameters.php index b6f84c1dd48..161bc477d84 100644 --- a/LibreNMS/Data/Graphing/GraphParameters.php +++ b/LibreNMS/Data/Graphing/GraphParameters.php @@ -284,8 +284,8 @@ private function graphColors(): array private function extractType(string $type): array { preg_match('/^(?P[A-Za-z0-9]+)_(?P.+)/', $type, $graphtype); - $type = basename($graphtype['type']); - $subtype = basename($graphtype['subtype']); + $type = basename($graphtype['type'] ?? ''); + $subtype = basename($graphtype['subtype'] ?? ''); return [$type, $subtype]; } diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index 76ee10f0b15..d3f4bae3ad4 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -27,8 +27,6 @@ namespace LibreNMS\Data\Graphing; use App\Facades\DeviceCache; -use App\Models\Device; -use App\Models\Port; use LibreNMS\Exceptions\InvalidGraph; class LegacyGraph extends AbstractGraph @@ -42,8 +40,6 @@ class LegacyGraph extends AbstractGraph private array $rrdFiles = []; private bool $loaded = false; private array $rrdOptions = []; - private ?Device $device = null; - private ?Port $port = null; /** * @param array $vars @@ -51,18 +47,22 @@ class LegacyGraph extends AbstractGraph * @throws InvalidGraph */ public function __construct( - public readonly string $type, - public readonly string $subtype, - private readonly array $vars = [], + GraphParameters $params, + array $vars = [], ) { - $this->auth_file = base_path("includes/html/graphs/$this->type/auth.inc.php"); - $graph_file = base_path("includes/html/graphs/$this->type/$this->subtype.inc.php"); + parent::__construct($params, $vars); + + $this->auth_file = base_path("includes/html/graphs/$params->type/auth.inc.php"); + if (! file_exists($this->auth_file)) { + throw new InvalidGraph; + } + + $graph_file = base_path("includes/html/graphs/$params->type/$params->subtype.inc.php"); if (! file_exists($graph_file)) { - $graph_file = base_path("includes/html/graphs/$this->type/generic.inc.php"); + $graph_file = base_path("includes/html/graphs/$params->type/generic.inc.php"); } $this->graph_file = $graph_file; - - if (! file_exists($this->auth_file) || ! file_exists($this->graph_file)) { + if (! file_exists($this->graph_file)) { throw new InvalidGraph; } } @@ -78,18 +78,10 @@ private function load(): void include_once base_path('includes/dbFacile.php'); include_once base_path('includes/rewrites.php'); - if (! $this->device && isset($this->vars['device'])) { - $this->device = DeviceCache::get($this->vars['device']); - } - - if ($this->device) { + if ($this->device->exists) { DeviceCache::setPrimary($this->device->device_id); } - if (! $this->port && isset($this->vars['id']) && $this->type === 'port') { - $this->port = \App\Facades\PortCache::get($this->vars['id']); - } - // Local scope variables for the included files $device = $this->device; $port = $this->port; @@ -108,7 +100,7 @@ private function load(): void return; } - $graph_params = app()->bound(GraphParameters::class) ? app(GraphParameters::class) : new GraphParameters($this->vars); + $graph_params = $this->params; $type = $graph_params->type; $subtype = $graph_params->subtype; $height = $graph_params->height; @@ -150,7 +142,7 @@ public function authorize(): bool return $this->authorized; } - public function rrdDefinition(GraphParameters $graph_params): array + public function rrdDefinition(): array { $this->load(); diff --git a/LibreNMS/Graphs/Device/NetstatIpGraph.php b/LibreNMS/Graphs/Device/NetstatIpGraph.php index e0e38931f2e..149375a8b23 100644 --- a/LibreNMS/Graphs/Device/NetstatIpGraph.php +++ b/LibreNMS/Graphs/Device/NetstatIpGraph.php @@ -2,30 +2,19 @@ namespace LibreNMS\Graphs\Device; -use App\Facades\DeviceCache; use App\Facades\Rrd; -use App\Models\Device; use Illuminate\Support\Facades\Gate; use LibreNMS\Data\Graphing\AbstractGraph; use LibreNMS\Data\Graphing\Builders\MultiLineGraphBuilder; -use LibreNMS\Data\Graphing\GraphParameters; class NetstatIpGraph extends AbstractGraph { - private Device $device; - - public function __construct( - private readonly array $vars = [], - ) { - $this->device = DeviceCache::get($this->vars['device'] ?? null); - } - public function authorize(): bool { return Gate::allows('view', $this->device); } - public function rrdDefinition(GraphParameters $graph_params): array + public function rrdDefinition(): array { $rrd_file = Rrd::name($this->device->hostname, 'netstats-ip'); @@ -40,7 +29,7 @@ public function rrdDefinition(GraphParameters $graph_params): array ->addDataset($rrd_file, 'ipInDiscards', 'In Discards') ->addDataset($rrd_file, 'ipOutDiscards', 'Out Discards', invert: true) ->addDataset($rrd_file, 'ipOutNoRoutes', 'Out No Routes', invert: true) - ->build($graph_params); + ->build($this->params); } public function getGraphTitle(): string diff --git a/LibreNMS/Graphs/Device/ProcessorGraph.php b/LibreNMS/Graphs/Device/ProcessorGraph.php index 5888f5f9453..d17e5ed7fe2 100644 --- a/LibreNMS/Graphs/Device/ProcessorGraph.php +++ b/LibreNMS/Graphs/Device/ProcessorGraph.php @@ -26,29 +26,25 @@ namespace LibreNMS\Graphs\Device; -use App\Facades\DeviceCache; use App\Facades\LibrenmsConfig; use App\Facades\Rrd; -use App\Models\Device; +use App\Models\Processor; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Gate; use LibreNMS\Data\Graphing\AbstractGraph; use LibreNMS\Data\Graphing\Builders\MultiLineGraphBuilder; use LibreNMS\Data\Graphing\Builders\MultiSimplexSeparatedGraphBuilder; -use LibreNMS\Data\Graphing\GraphParameters; class ProcessorGraph extends AbstractGraph { public string $type = 'device'; public string $subtype = 'processor'; - private Device $device; + /** @var Collection */ private Collection $processors; - public function __construct( - private readonly array $vars = [], - ) { - $this->device = DeviceCache::get($this->vars['device'] ?? null); + protected function init(): void + { $this->processors = $this->device->exists ? $this->device->processors : new Collection; } @@ -76,7 +72,7 @@ public function getRrdFiles(): array return $files; } - public function rrdDefinition(GraphParameters $graph_params): array + public function rrdDefinition(): array { if ($this->processors->isEmpty()) { throw new \LibreNMS\Exceptions\RrdGraphException('No Processors'); @@ -111,7 +107,7 @@ public function rrdDefinition(GraphParameters $graph_params): array $builder->addDataset($dataset['filename'], 'usage', $dataset['descr']); } - return $builder->build($graph_params); + return $builder->build($this->params); } $builder = (new MultiLineGraphBuilder()) @@ -126,6 +122,6 @@ public function rrdDefinition(GraphParameters $graph_params): array $builder->addDataset($dataset['filename'], 'usage', $dataset['descr'], area: true); } - return $builder->build($graph_params); + return $builder->build($this->params); } } diff --git a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php index d7e08998a52..b67c8cb4b92 100644 --- a/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php +++ b/LibreNMS/Interfaces/Data/Graphing/GraphInterface.php @@ -34,7 +34,9 @@ public function authorize(): bool; public function validation(): array; - public function rrdDefinition(GraphParameters $graph_params): array; + public function rrdDefinition(): array; + + public function getParams(): GraphParameters; public function getPageTitle(): string; diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index ea22b1f0792..255efa94cc4 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -29,10 +29,9 @@ use App\Facades\LibrenmsConfig; use App\Models\Device; use Illuminate\Support\Arr; -use Illuminate\Validation\ValidationException; +use Illuminate\Support\Facades\Validator; use LibreNMS\Data\Graphing\GraphFactory; use LibreNMS\Data\Graphing\GraphImage; -use LibreNMS\Data\Graphing\GraphParameters; use LibreNMS\Enum\ImageFormat; use LibreNMS\Exceptions\InvalidGraph; use LibreNMS\Exceptions\RrdGraphException; @@ -104,37 +103,28 @@ public static function getImage($vars): GraphImage */ public static function get(array|string $vars): GraphImage { - // handle possible graph url input - if (is_string($vars)) { - $vars = Url::parseLegacyPathVars($vars); - } - - $graph_params = new GraphParameters($vars); - app()->instance(GraphParameters::class, $graph_params); - $name = $graph_params->type . '_' . $graph_params->subtype; + $vars = is_string($vars) ? Url::parseLegacyPathVars($vars) : $vars; - /** @var GraphFactory $factory */ - $factory = app(GraphFactory::class); - $graph = $factory->graphFor($name, $vars); + $graph = app(GraphFactory::class)->graphFor($vars['type'] ?? '', $vars); + $params = $graph->getParams(); $rrd_options = self::getRrdOptions($vars, $rrd_filename); // Generating the graph! try { - $image_data = Rrd::graph($rrd_options); - - return new GraphImage($graph_params->imageFormat, $graph->getGraphTitle(), $image_data); + return new GraphImage($params->imageFormat, $graph->getGraphTitle(), Rrd::graph($rrd_options)); } catch (RrdGraphException $e) { - // preserve original error if debug is enabled, otherwise make it a little more user friendly if (Debug::isEnabled()) { throw $e; } - if ($rrd_filename && ! Rrd::checkRrdExists($rrd_filename)) { - throw new RrdGraphException('No Data file' . basename($rrd_filename), 'No Data', $graph_params->width, $graph_params->height, $e->getCode(), $e->getImage()); + foreach ($graph->getRrdFiles() as $filename) { + if (! Rrd::checkRrdExists($filename)) { + throw new RrdGraphException('No Data file' . basename($filename), 'No Data', $params->width, $params->height, $e->getCode(), $e->getImage()); + } } - throw new RrdGraphException('Error: ' . $e->getMessage(), 'Draw Error', $graph_params->width, $graph_params->height, $e->getCode(), $e->getImage()); + throw new RrdGraphException('Error: ' . $e->getMessage(), 'Draw Error', $params->width, $params->height, $e->getCode(), $e->getImage()); } } @@ -150,45 +140,29 @@ public static function get(array|string $vars): GraphImage */ public static function getRrdOptions(array|string $vars, ?string &$rrd_filename = null): array { - // handle possible graph url input - if (is_string($vars)) { - $vars = Url::parseLegacyPathVars($vars); - } + $vars = is_string($vars) ? Url::parseLegacyPathVars($vars) : $vars; - $graph_params = new GraphParameters($vars); - $name = $graph_params->type . '_' . $graph_params->subtype; + $graph = app(GraphFactory::class)->graphFor($vars['type'] ?? '', $vars); + $params = $graph->getParams(); - /** @var GraphFactory $factory */ - $factory = app(GraphFactory::class); - $graph = $factory->graphFor($name, $vars); - - // Run validation if rules are defined if ($rules = $graph->validation()) { - $validator = \Validator::make($vars, $rules); - if ($validator->fails()) { - throw new ValidationException($validator); - } - $vars = $validator->validated(); - } - - if ($graph instanceof \LibreNMS\Data\Graphing\AbstractGraph) { - $graph->fill($vars); + Validator::validate($vars, $rules); } if (! $graph->authorize()) { - throw new RrdGraphException('No Authorization', 'No Auth', $graph_params->width, $graph_params->height); + throw new RrdGraphException('No Authorization', 'No Auth', $params->width, $params->height); } - $rrd_options = $graph->rrdDefinition($graph_params); + $rrd_options = $graph->rrdDefinition(); if (empty($rrd_options)) { - throw new RrdGraphException('Graph Definition Error', 'Def Error', $graph_params->width, $graph_params->height); + throw new RrdGraphException('Graph Definition Error', 'Def Error', $params->width, $params->height); } $rrdFiles = $graph->getRrdFiles(); $rrd_filename = reset($rrdFiles) ?: null; - return [...$graph_params->toRrdOptions(), ...$rrd_options]; + return [...$params->toRrdOptions(), ...$rrd_options]; } public static function getTypes(): array From ad80ce1b11861e465a32b55ab600baf6a89e7183 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Thu, 2 Jul 2026 10:08:34 -0500 Subject: [PATCH 13/16] fix some issues --- app/Console/Commands/GraphDetail.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/Console/Commands/GraphDetail.php b/app/Console/Commands/GraphDetail.php index 2e8f18fb4b6..6a61141b221 100644 --- a/app/Console/Commands/GraphDetail.php +++ b/app/Console/Commands/GraphDetail.php @@ -9,7 +9,7 @@ class GraphDetail extends Command { - protected $signature = 'graph:detail {name} {--device=}'; + protected $signature = 'graph:detail {name} {--device=} {--full}'; protected $description = 'Show info about a given graph'; public function handle(GraphFactory $graphs): int @@ -21,13 +21,15 @@ public function handle(GraphFactory $graphs): int 'device' => $device->device_id, ]); - $this->line("Type: $graph->type"); - $this->line("Subtype: $graph->subtype"); + $trim = $this->option('full') ? null : 80; + + $this->line("Type: {$graph->getParams()->title}"); + $this->line("Subtype: {$graph->getParams()->subtype}"); $this->line('Authorized: ' . ($graph->authorize() ? 'true' : 'false')); $this->line('Graph Title: ' . $graph->getGraphTitle()); - $this->line('Page Title: ' . substr($graph->getPageTitle(), 0, 80)); + $this->line('Page Title: ' . substr($graph->getPageTitle(), 0, $trim)); $this->line('Rrd Files: ' . implode(', ', array_map(fn ($f) => basename($f), $graph->getRrdFiles()))); - $this->line('Definition: ' . substr(implode(' ', $graph->rrdDefinition()), 80)); + $this->line('Definition: ' . substr(implode(' ', $graph->rrdDefinition()), $trim)); return 0; } From bcdc5e2b679fb3d492c3252de0ecb2d691b7152b Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 8 Jul 2026 12:50:47 -0500 Subject: [PATCH 14/16] Fixes after merge --- LibreNMS/Data/Graphing/AbstractGraph.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LibreNMS/Data/Graphing/AbstractGraph.php b/LibreNMS/Data/Graphing/AbstractGraph.php index c8aaccca112..39a6bfa83cf 100644 --- a/LibreNMS/Data/Graphing/AbstractGraph.php +++ b/LibreNMS/Data/Graphing/AbstractGraph.php @@ -47,6 +47,8 @@ public function __construct( $port_id = $this->vars['port'] ?? ($this->params->type == 'port' ? ($this->vars['id'] ?? null) : null); $this->port = PortCache::get($port_id); + + $this->init(); } public function getParams(): GraphParameters @@ -58,7 +60,7 @@ public function validation(): array { return [ 'type' => ['required', 'string', 'regex:/^[a-z][a-z0-9]*_[a-zA-Z0-9_]+$/'], - 'id' => ['nullable', 'integer'], + 'id' => ['nullable', 'regex:/^[A-Za-z0-9,._-]+$/'], 'from' => ['nullable', 'regex:/^(-?\d+|-?\d+[smhdwMy]|now|end)$/'], 'to' => ['nullable', 'regex:/^(-?\d+|-?\d+[smhdwMy]|now|end)$/'], 'width' => ['nullable', 'integer', 'min:10', 'max:10000'], From 82f8cb23d8b6f8a850484211acb9fa05ab1c6aa6 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Wed, 8 Jul 2026 14:31:24 -0500 Subject: [PATCH 15/16] chdir --- LibreNMS/Data/Graphing/LegacyGraph.php | 126 ++++++++++++++----------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/LibreNMS/Data/Graphing/LegacyGraph.php b/LibreNMS/Data/Graphing/LegacyGraph.php index d3f4bae3ad4..b4a687f5d94 100644 --- a/LibreNMS/Data/Graphing/LegacyGraph.php +++ b/LibreNMS/Data/Graphing/LegacyGraph.php @@ -28,6 +28,7 @@ use App\Facades\DeviceCache; use LibreNMS\Exceptions\InvalidGraph; +use LibreNMS\Exceptions\RrdNotFoundException; class LegacyGraph extends AbstractGraph { @@ -73,66 +74,77 @@ private function load(): void return; } - include_once base_path('includes/common.php'); - include_once base_path('includes/html/functions.inc.php'); - include_once base_path('includes/dbFacile.php'); - include_once base_path('includes/rewrites.php'); + $previousCwd = getcwd(); + chdir(base_path()); + + try { + include_once base_path('includes/common.php'); + include_once base_path('includes/html/functions.inc.php'); + include_once base_path('includes/dbFacile.php'); + include_once base_path('includes/rewrites.php'); + + if ($this->device->exists) { + DeviceCache::setPrimary($this->device->device_id); + } + + // Local scope variables for the included files + $device = $this->device; + $port = $this->port; + $vars = $this->vars; + + $auth = auth()->guest(); + @include $this->auth_file; + + $this->authorized = $auth; + $this->graphTitle = $graph_title ?? $this->graphTitle; + $this->pageTitle = $title ?? $this->graphTitle; + + if (! $auth) { + $this->loaded = true; + + return; + } + + $graph_params = $this->params; + $type = $graph_params->type; + $subtype = $graph_params->subtype; + $height = $graph_params->height; + $width = $graph_params->width; + $from = $graph_params->from; + $to = $graph_params->to; + $period = $graph_params->period; + $prev_from = $graph_params->prev_from; + $inverse = $graph_params->inverse; + $in = $graph_params->in; + $out = $graph_params->out; + $float_precision = $graph_params->float_precision; + $title = $graph_params->visible('title'); + $nototal = ! $graph_params->visible('total'); + $nodetails = ! $graph_params->visible('details'); + $noagg = ! $graph_params->visible('aggregate'); + + $rrd_options = []; + + @include $this->graph_file; + + $this->rrdOptions = $rrd_options; + + if (isset($rrd_list) && is_array($rrd_list)) { + $this->rrdFiles = array_column($rrd_list, 'filename'); + } elseif (isset($rrd_filenames) && is_array($rrd_filenames)) { + $this->rrdFiles = $rrd_filenames; + } else { + $this->rrdFiles = isset($rrd_filename) ? [$rrd_filename] : []; + } - if ($this->device->exists) { - DeviceCache::setPrimary($this->device->device_id); - } - - // Local scope variables for the included files - $device = $this->device; - $port = $this->port; - $vars = $this->vars; - - $auth = auth()->guest(); - include $this->auth_file; - - $this->authorized = $auth; - $this->graphTitle = $graph_title ?? $this->graphTitle; - $this->pageTitle = $title ?? $this->graphTitle; - - if (! $auth) { $this->loaded = true; - - return; + } catch (RrdNotFoundException) { + // + } finally { + if ($previousCwd !== false) { + chdir($previousCwd); + } } - - $graph_params = $this->params; - $type = $graph_params->type; - $subtype = $graph_params->subtype; - $height = $graph_params->height; - $width = $graph_params->width; - $from = $graph_params->from; - $to = $graph_params->to; - $period = $graph_params->period; - $prev_from = $graph_params->prev_from; - $inverse = $graph_params->inverse; - $in = $graph_params->in; - $out = $graph_params->out; - $float_precision = $graph_params->float_precision; - $title = $graph_params->visible('title'); - $nototal = ! $graph_params->visible('total'); - $nodetails = ! $graph_params->visible('details'); - $noagg = ! $graph_params->visible('aggregate'); - - $rrd_options = []; - - include $this->graph_file; - - $this->rrdOptions = $rrd_options; - - if (isset($rrd_list) && is_array($rrd_list)) { - $this->rrdFiles = array_column($rrd_list, 'filename'); - } elseif (isset($rrd_filenames) && is_array($rrd_filenames)) { - $this->rrdFiles = $rrd_filenames; - } else { - $this->rrdFiles = isset($rrd_filename) ? [$rrd_filename] : []; - } - - $this->loaded = true; } public function authorize(): bool From 49588f2f05e06c8de045e408cff437dec4a3ea34 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 8 Jul 2026 19:32:14 +0000 Subject: [PATCH 16/16] Apply fixes from StyleCI --- LibreNMS/Data/Graphing/AbstractGraph.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LibreNMS/Data/Graphing/AbstractGraph.php b/LibreNMS/Data/Graphing/AbstractGraph.php index 39a6bfa83cf..69c1db450f5 100644 --- a/LibreNMS/Data/Graphing/AbstractGraph.php +++ b/LibreNMS/Data/Graphing/AbstractGraph.php @@ -40,8 +40,7 @@ abstract class AbstractGraph implements GraphInterface public function __construct( protected readonly GraphParameters $params, protected readonly array $vars = [], - ) - { + ) { $device_id = $this->vars['device'] ?? ($this->params->type == 'device' ? ($this->vars['id'] ?? null) : null); $this->device = DeviceCache::get($device_id);