Skip to content

Commit

Permalink
Email Transport: embed graphs by default (librenms#14270)
Browse files Browse the repository at this point in the history
* Email embed graphs

* Allow attachment for non-html
Add setting to webui
Correct $auth setting

* Cleanups, throw RrdGraphException instead of returning an error image.
Generate the error image later, giving more control.
Reduce code duplication a little

* Style and lint fixes
Change to flags

* Add baseline for lint errors I don't know how to resolve

* oopsie, changed the code after generating the baseline

* Tiny cleanups.  Make set DeviceCache primary, it is free.

* Docs.

* email_html note

* Allow control of graph embed at the email transport level to override the global config.

* Allow control of graph embed at the email transport level to override the global config.

* Add INLINE_BASE64 to make it easier to create inline image tags
  • Loading branch information
murrant authored Sep 6, 2022
1 parent ec8629f commit 302a989
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 121 deletions.
9 changes: 8 additions & 1 deletion LibreNMS/Alert/Transport/Mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function contactMail($obj)
$msg = preg_replace("/(?<!\r)\n/", "\r\n", $obj['msg']);
}

return \LibreNMS\Util\Mail::send($email, $obj['title'], $msg, $html);
return \LibreNMS\Util\Mail::send($email, $obj['title'], $msg, $html, $this->config['attach-graph'] ?? null);
}

public static function configTemplate()
Expand All @@ -59,6 +59,13 @@ public static function configTemplate()
'descr' => 'Email address of contact',
'type' => 'text',
],
[
'title' => 'Include Graphs',
'name' => 'attach-graph',
'descr' => 'Include graph image data in the email. Will be embedded if html5, otherwise attached. Template must use @signedGraphTag',
'type' => 'checkbox',
'default' => true,
],
],
'validation' => [
'email' => 'required|email',
Expand Down
16 changes: 11 additions & 5 deletions LibreNMS/Data/Store/Rrd.php
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,6 @@ public function purge($hostname, $prefix)
* @param string $options
* @return string
*
* @throws \LibreNMS\Exceptions\FileExistsException
* @throws \LibreNMS\Exceptions\RrdGraphException
*/
public function graph(string $options): string
Expand All @@ -568,9 +567,13 @@ public function graph(string $options): string
$process->setTimeout(300);
$process->setIdleTimeout(300);

$command = $this->buildCommand('graph', '-', $options);
$process->setInput($command . "\nquit");
$process->run();
try {
$command = $this->buildCommand('graph', '-', $options);
$process->setInput($command . "\nquit");
$process->run();
} catch (FileExistsException $e) {
throw new RrdGraphException($e->getMessage(), 'File Exists');
}

$feedback_position = strrpos($process->getOutput(), 'OK ');
if ($feedback_position !== false) {
Expand All @@ -584,14 +587,17 @@ public function graph(string $options): string
$position += strlen($search);
throw new RrdGraphException(
substr($process->getOutput(), $position),
null,
null,
null,
$process->getExitCode(),
substr($process->getOutput(), 0, $position)
);
}

// only error text was returned
$error = trim($process->getOutput() . PHP_EOL . $process->getErrorOutput());
throw new RrdGraphException($error, $process->getExitCode(), '');
throw new RrdGraphException($error, null, null, null, $process->getExitCode());
}

private function getImageEnd(string $type): string
Expand Down
33 changes: 31 additions & 2 deletions LibreNMS/Exceptions/RrdGraphException.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,48 @@
namespace LibreNMS\Exceptions;

use Exception;
use LibreNMS\Util\Graph;

class RrdGraphException extends Exception
{
/** @var string */
protected $image_output;
/** @var string|null */
private $short_text;
/** @var int|string|null */
private $width;
/** @var int|string|null */
private $height;

public function __construct($error, $exit_code, $image_output)
/**
* @param string $error
* @param string|null $short_text
* @param int|string|null $width
* @param int|string|null $height
* @param int $exit_code
* @param string $image_output
*/
public function __construct($error, $short_text = null, $width = null, $height = null, $exit_code = 0, $image_output = '')
{
parent::__construct($error, $exit_code);
$this->short_text = $short_text;
$this->image_output = $image_output;
$this->width = $width;
$this->height = $height;
}

public function getImage()
public function getImage(): string
{
return $this->image_output;
}

public function generateErrorImage(): string
{
return Graph::error(
$this->getMessage(),
$this->short_text,
empty($this->width) ? 300 : (int) $this->width,
empty($this->height) ? null : (int) $this->height,
);
}
}
207 changes: 204 additions & 3 deletions LibreNMS/Util/Graph.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,147 @@

namespace LibreNMS\Util;

use App\Facades\DeviceCache;
use App\Models\Device;
use Illuminate\Support\Facades\Auth;
use LibreNMS\Config;
use LibreNMS\Exceptions\RrdGraphException;
use Rrd;

class Graph
{
public static function getTypes()
const BASE64_OUTPUT = 1; // BASE64 encoded image data
const INLINE_BASE64 = 2; // img src inline base64 image
const COMMAND_ONLY = 4; // just print the command

/**
* Fetch a graph image (as string) based on the given $vars
* Optionally, override the output format to base64
*
* @param array|string $vars
* @param int $flags Flags for controlling graph generating options.
* @return string
*
* @throws \LibreNMS\Exceptions\RrdGraphException
*/
public static function get($vars, int $flags = 0): string
{
define('IGNORE_ERRORS', true);
chdir(base_path());

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);
}

[$type, $subtype] = extract_graph_type($vars['type']);

$graph_title = '';
if (isset($vars['device'])) {
$device = device_by_id_cache(is_numeric($vars['device']) ? $vars['device'] : getidbyname($vars['device']));
DeviceCache::setPrimary($device['device_id']);

//set default graph title
$graph_title = DeviceCache::getPrimary()->displayName();
}

// variables for included graphs
$width = $vars['width'] ?? 400;
$height = $vars['height'] ?? $width / 3;
$title = $vars['title'] ?? '';
$vertical = $vars['vertical'] ?? '';
$legend = $vars['legend'] ?? false;
$output = $vars['output'] ?? 'default';
$from = parse_at_time($vars['from'] ?? '-1d');
$to = empty($vars['to']) ? time() : parse_at_time($vars['to']);
$period = ($to - $from);
$prev_from = ($from - $period);
$graph_image_type = $vars['graph_type'] ?? Config::get('webui.graph_type');
Config::set('webui.graph_type', $graph_image_type); // set in case accessed elsewhere
$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);
}

if (is_customoid_graph($type, $subtype)) {
$unit = $vars['unit'];
require base_path('/includes/html/graphs/customoid/customoid.inc.php');
} elseif (is_file(base_path("/includes/html/graphs/$type/$subtype.inc.php"))) {
require base_path("/includes/html/graphs/$type/$subtype.inc.php");
} else {
throw new RrdGraphException("{$type}_$subtype template missing", "{$type}_$subtype missing", $width, $height);
}

if ($graph_image_type === 'svg') {
$rrd_options .= ' --imgformat=SVG';
if ($width < 350) {
$rrd_options .= ' -m 0.75 -R light';
}
}

// command output requested
if ($flags & self::COMMAND_ONLY) {
$cmd_output = "<div class='infobox'>";
$cmd_output .= "<p style='font-size: 16px; font-weight: bold;'>RRDTool Command</p>";
$cmd_output .= "<pre class='rrd-pre'>";
$cmd_output .= escapeshellcmd('rrdtool ' . Rrd::buildCommand('graph', Config::get('temp_dir') . '/' . strgen(), $rrd_options));
$cmd_output .= '</pre>';
try {
$cmd_output .= Rrd::graph($rrd_options);
} catch (RrdGraphException $e) {
$cmd_output .= "<p style='font-size: 16px; font-weight: bold;'>RRDTool Output</p>";
$cmd_output .= "<pre class='rrd-pre'>";
$cmd_output .= $e->getMessage();
$cmd_output .= '</pre>';
}
$cmd_output .= '</div>';

return $cmd_output;
}

if (empty($rrd_options)) {
throw new RrdGraphException('Graph Definition Error', 'Def Error', $width, $height);
}

// Generating the graph!
try {
$image_data = Rrd::graph($rrd_options);

// output the graph int the desired format
if (Debug::isEnabled()) {
return '<img src="data:' . self::imageType($graph_image_type) . ';base64,' . base64_encode($image_data) . '" alt="graph" />';
} elseif ($flags & self::BASE64_OUTPUT || $output == 'base64') {
return base64_encode($image_data);
} elseif ($flags & self::INLINE_BASE64 || $output == 'inline-base64') {
return 'data:' . self::imageType($graph_image_type) . ';base64,' . base64_encode($image_data);
}

return $image_data; // raw 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)) {
throw new RrdGraphException('No Data file' . basename($rrd_filename), 'No Data', $width, $height, $e->getCode(), $e->getImage());
}

throw new RrdGraphException('Error: ' . $e->getMessage(), 'Draw Error', $width, $height, $e->getCode(), $e->getImage());
}
}

public static function getTypes(): array
{
return ['device', 'port', 'application', 'munin', 'service'];
}
Expand All @@ -42,7 +177,7 @@ public static function getTypes()
* @param Device $device
* @return array
*/
public static function getSubtypes($type, $device = null)
public static function getSubtypes($type, $device = null): array
{
$types = [];

Expand Down Expand Up @@ -79,7 +214,7 @@ public static function getSubtypes($type, $device = null)
* @param string $subtype
* @return bool
*/
public static function isMibGraph($type, $subtype)
public static function isMibGraph($type, $subtype): bool
{
return Config::get("graph_types.$type.$subtype.section") == 'mib';
}
Expand All @@ -98,4 +233,70 @@ public static function getOverviewGraphsForDevice($device)

return Config::get("os_group.$os_group.over", Config::get('os.default.over'));
}

/**
* Get the http content type of the image
*
* @param string $type svg or png
* @return string
*/
public static function imageType(string $type): string
{
return $type === 'svg' ? 'image/svg+xml' : 'image/png';
}

/**
* Create image to output text instead of a graph.
*
* @param string $text Error message to display
* @param string|null $short_text Error message for smaller graph images
* @param int $width Width of graph image (defaults to 300)
* @param int|null $height Height of graph image (defaults to width / 3)
* @param int[] $color Color of text, defaults to dark red
* @return string the generated image
*/
public static function error(string $text, ?string $short_text, int $width = 300, ?int $height = null, array $color = [128, 0, 0]): string
{
$type = Config::get('webui.graph_type');
$height = $height ?? $width / 3;

if ($short_text !== null && $width < 200) {
$text = $short_text;
}

if ($type === 'svg') {
$rgb = implode(', ', $color);

return <<<SVG
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
viewBox="0 0 $width $height"
preserveAspectRatio="xMinYMin">
<foreignObject x="0" y="0" width="$width" height="$height" transform="translate(0,0)">
<xhtml:div style="display:table; width:{$width}px; height:{$height}px; overflow:hidden;">
<xhtml:div style="display:table-cell; vertical-align:middle;">
<xhtml:div style="color:rgb($rgb); text-align:center; font-family:sans-serif; font-size:0.6em;">$text</xhtml:div>
</xhtml:div>
</xhtml:div>
</foreignObject>
</svg>
SVG;
}

$img = imagecreate($width, $height);
imagecolorallocatealpha($img, 255, 255, 255, 127); // transparent background

$px = (int) ((imagesx($img) - 7.5 * strlen($text)) / 2);
$font = $width < 200 ? 3 : 5;
imagestring($img, $font, $px, ($height / 2 - 8), $text, imagecolorallocate($img, ...$color));

// Output the image
ob_start();
imagepng($img);
$output = ob_get_clean();
ob_end_clean();
imagedestroy($img);

return $output;
}
}
Loading

0 comments on commit 302a989

Please sign in to comment.