summaryrefslogtreecommitdiffstats
path: root/library
diff options
context:
space:
mode:
Diffstat (limited to 'library')
-rw-r--r--library/Cube/Cube.php341
-rw-r--r--library/Cube/CubeRenderer.php512
-rw-r--r--library/Cube/CubeRenderer/HostStatusCubeRenderer.php143
-rw-r--r--library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php149
-rw-r--r--library/Cube/Dimension.php70
-rw-r--r--library/Cube/DimensionParams.php85
-rw-r--r--library/Cube/Hook/ActionsHook.php99
-rw-r--r--library/Cube/Hook/IcingaDbActionsHook.php125
-rw-r--r--library/Cube/IcingaDb/CustomVariableDimension.php166
-rw-r--r--library/Cube/IcingaDb/IcingaDbCube.php338
-rw-r--r--library/Cube/IcingaDb/IcingaDbHostStatusCube.php80
-rw-r--r--library/Cube/IcingaDb/IcingaDbServiceStatusCube.php94
-rw-r--r--library/Cube/Ido/CustomVarDimension.php146
-rw-r--r--library/Cube/Ido/DataView/Hoststatus.php17
-rw-r--r--library/Cube/Ido/DbCube.php298
-rw-r--r--library/Cube/Ido/IdoCube.php219
-rw-r--r--library/Cube/Ido/IdoHostStatusCube.php97
-rw-r--r--library/Cube/Ido/IdoServiceStatusCube.php97
-rw-r--r--library/Cube/Ido/Query/HoststatusQuery.php47
-rw-r--r--library/Cube/Ido/ZfSelectWrapper.php77
-rw-r--r--library/Cube/ProvidedHook/Cube/IcingaDbActions.php48
-rw-r--r--library/Cube/ProvidedHook/Cube/MonitoringActions.php53
-rw-r--r--library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php11
-rw-r--r--library/Cube/Web/ActionLink.php103
-rw-r--r--library/Cube/Web/ActionLinks.php115
-rw-r--r--library/Cube/Web/Controller.php297
-rw-r--r--library/Cube/Web/IdoController.php198
27 files changed, 4025 insertions, 0 deletions
diff --git a/library/Cube/Cube.php b/library/Cube/Cube.php
new file mode 100644
index 0000000..1e688f0
--- /dev/null
+++ b/library/Cube/Cube.php
@@ -0,0 +1,341 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Web\View;
+
+abstract class Cube
+{
+ /** @var ?string Prefix for slice params */
+ public const SLICE_PREFIX = null;
+
+ /** @var ?bool Whether the icingadb backend is in use */
+ public const IS_USING_ICINGADB = null;
+
+ /** @var array<string, Dimension> Available dimensions */
+ protected $availableDimensions;
+
+ /** @var array Fact names */
+ protected $chosenFacts;
+
+ /** @var Dimension[] */
+ protected $dimensions = array();
+
+ protected $slices = array();
+
+ protected $renderer;
+
+ abstract public function fetchAll();
+
+ /**
+ * Get whether the icingadb backend is in use
+ *
+ * @return bool
+ */
+ public static function isUsingIcingaDb(): bool
+ {
+ return static::IS_USING_ICINGADB
+ ?? (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend());
+ }
+
+ public function removeDimension($name)
+ {
+ unset($this->dimensions[$name]);
+ unset($this->slices[$name]);
+ return $this;
+ }
+
+ /**
+ * @return CubeRenderer
+ * @throws IcingaException
+ */
+ public function getRenderer()
+ {
+ throw new IcingaException('Got no cube renderer');
+ }
+
+ public function getPathLabel()
+ {
+ $dimensions = $this->getDimensionsLabel();
+ $slices = $this->getSlicesLabel();
+ $parts = array();
+ if ($dimensions !== null) {
+ $parts[] = $dimensions;
+ }
+
+ if ($slices !== null) {
+ $parts[] = $slices;
+ }
+
+ return implode(', ', $parts);
+ }
+
+ public function getDimensionsLabel()
+ {
+ $dimensions = $this->listDimensions();
+ if (empty($dimensions)) {
+ return null;
+ }
+
+ return implode(' -> ', array_map(function ($d) {
+ return $d->getLabel();
+ }, $dimensions));
+ }
+
+ public function getSlicesLabel()
+ {
+ $parts = array();
+
+ $slices = $this->getSlices();
+ if (empty($slices)) {
+ return null;
+ }
+ foreach ($slices as $key => $value) {
+ $parts[] = sprintf('%s = %s', $this->getDimension($key)->getLabel(), $value);
+ }
+
+ return implode(', ', $parts);
+ }
+
+ /**
+ * Create a new dimension
+ *
+ * @param string $name
+ * @return Dimension
+ */
+ abstract public function createDimension($name);
+
+ protected function registerAvailableDimensions()
+ {
+ if ($this->availableDimensions !== null) {
+ return;
+ }
+
+ $this->availableDimensions = [];
+ foreach ($this->listAvailableDimensions() as $name => $label) {
+ if (! isset($this->availableDimensions[$name])) {
+ $this->availableDimensions[$name] = $this->createDimension($name)->setLabel($label);
+ } else {
+ $this->availableDimensions[$name]->addLabel($label);
+ }
+ }
+ }
+
+ public function listAdditionalDimensions()
+ {
+ $this->registerAvailableDimensions();
+
+ $list = [];
+ foreach ($this->availableDimensions as $name => $dimension) {
+ if (! $this->hasDimension($name)) {
+ $list[$name] = $dimension->getLabel();
+ }
+ }
+
+ return $list;
+ }
+
+ abstract public function listAvailableDimensions();
+
+ public function getDimensionAfter($name)
+ {
+ $found = false;
+ $after = null;
+
+ foreach ($this->listDimensions() as $k => $d) {
+ if ($found) {
+ $after = $d;
+ break;
+ }
+
+ if ($k === $name) {
+ $found = true;
+ }
+ }
+
+ return $after;
+ }
+
+ public function listDimensionsUpTo($name)
+ {
+ $res = array();
+ foreach ($this->listDimensions() as $d => $_) {
+ $res[] = $d;
+ if ($d === $name) {
+ break;
+ }
+ }
+
+ return $res;
+ }
+
+ public function moveDimensionUp($name)
+ {
+ $last = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($v === $name) {
+ $found = $k;
+ break;
+ }
+
+ $last = $k;
+ }
+
+ if ($found !== null) {
+ $this->flipPositions($positions, $last, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ public function moveDimensionDown($name)
+ {
+ $next = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($found !== null) {
+ $next = $k;
+ break;
+ }
+
+ if ($v === $name) {
+ $found = $k;
+ }
+ }
+
+ if ($next !== null) {
+ $this->flipPositions($positions, $next, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ protected function flipPositions(&$array, $pos1, $pos2)
+ {
+ list(
+ $array[$pos1],
+ $array[$pos2]
+ ) = array(
+ $array[$pos2],
+ $array[$pos1]
+ );
+ }
+
+ protected function reOrderDimensions($positions)
+ {
+ $dimensions = array();
+ foreach ($positions as $pos => $key) {
+ $dimensions[$key] = $this->dimensions[$key];
+ }
+
+ $this->dimensions = $dimensions;
+ }
+
+ public function addDimension(Dimension $dimension)
+ {
+ $name = $dimension->getName();
+ if ($this->hasDimension($name)) {
+ throw new IcingaException('Cannot add dimension "%s" twice', $name);
+ }
+
+ $this->dimensions[$name] = $dimension;
+ return $this;
+ }
+
+ public function slice($key, $value)
+ {
+ if ($this->hasDimension($key)) {
+ $this->slices[$key] = $value;
+ } else {
+ throw new IcingaException('Got no such dimension: "%s"', $key);
+ }
+
+ return $this;
+ }
+
+ public function hasDimension($name)
+ {
+ return array_key_exists($name, $this->dimensions);
+ }
+
+ public function hasSlice($name)
+ {
+ return array_key_exists($name, $this->slices);
+ }
+
+ public function listSlices()
+ {
+ return array_keys($this->slices);
+ }
+
+ public function getSlices()
+ {
+ return $this->slices;
+ }
+
+ public function hasFact($name)
+ {
+ return array_key_exists($name, $this->chosenFacts);
+ }
+
+ public function getDimension($name)
+ {
+ return $this->dimensions[$name];
+ }
+
+ /**
+ * Return a list of chosen facts
+ *
+ * @return array
+ */
+ public function listFacts()
+ {
+ return $this->chosenFacts;
+ }
+
+ /**
+ * Choose a list of facts
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ $this->chosenFacts = $facts;
+ return $this;
+ }
+
+ public function listDimensions()
+ {
+ return array_diff_key($this->dimensions, $this->slices);
+ }
+
+ public function listColumns()
+ {
+ return array_merge(array_keys($this->listDimensions()), $this->listFacts());
+ }
+
+ /**
+ * @param View $view
+ * @param CubeRenderer $renderer
+ * @return string
+ */
+ public function render(View $view, CubeRenderer $renderer = null)
+ {
+ if ($renderer === null) {
+ $renderer = $this->getRenderer();
+ }
+
+ return $renderer->render($view);
+ }
+}
diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php
new file mode 100644
index 0000000..e88a938
--- /dev/null
+++ b/library/Cube/CubeRenderer.php
@@ -0,0 +1,512 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Web\View;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+use Generator;
+use Icinga\Data\Tree\TreeNode;
+
+/**
+ * CubeRenderer base class
+ *
+ * Every Cube Renderer must extend this class.
+ *
+ * TODO: Should we introduce DimensionRenderer, FactRenderer and SummaryHelper
+ * instead?
+ *
+ * @package Icinga\Module\Cube
+ */
+abstract class CubeRenderer
+{
+ /** @var View */
+ protected $view;
+
+ /** @var Cube */
+ protected $cube;
+
+ /** @var array Our dimensions */
+ protected $dimensions;
+
+ /** @var array Our dimensions in regular order */
+ protected $dimensionOrder;
+
+ /** @var array Our dimensions in reversed order as a quick lookup source */
+ protected $reversedDimensions;
+
+ /** @var array Level (deepness) for each dimension (0, 1, 2...) */
+ protected $dimensionLevels;
+
+ protected $facts;
+
+ /** @var object The row before the current one */
+ protected $lastRow;
+
+ /**
+ * Current summaries
+ *
+ * This is an object of objects, with dimension names being the keys and
+ * a facts row containing current (rollup) summaries for that dimension
+ * being it's value
+ *
+ * @var object
+ */
+ protected $summaries;
+
+ protected $started;
+
+ /**
+ * CubeRenderer constructor.
+ *
+ * @param Cube $cube
+ */
+ public function __construct(Cube $cube)
+ {
+ $this->cube = $cube;
+ }
+
+ /**
+ * Render the given facts
+ *
+ * @param $facts
+ * @return string
+ */
+ abstract public function renderFacts($facts);
+
+ /**
+ * Returns the base url for the details action
+ *
+ * @return string
+ */
+ abstract protected function getDetailsBaseUrl();
+
+ /**
+ * Get the severity sort columns
+ *
+ * @return Generator
+ */
+ abstract protected function getSeveritySortColumns(): Generator;
+
+ /**
+ * Initialize all we need
+ */
+ protected function initialize()
+ {
+ $this->started = false;
+ $this->initializeDimensions()
+ ->initializeFacts()
+ ->initializeLastRow()
+ ->initializeSummaries();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeLastRow()
+ {
+ $object = (object) array();
+ foreach ($this->dimensions as $dimension) {
+ $object->{$dimension->getName()} = null;
+ }
+
+ $this->lastRow = $object;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeDimensions()
+ {
+ $this->dimensions = $this->cube->listDimensions();
+
+ $min = 3;
+ $cnt = count($this->dimensions);
+ if ($cnt < $min) {
+ $pos = 0;
+ $diff = $min - $cnt;
+ $this->dimensionOrder = [];
+ foreach ($this->dimensions as $name => $_) {
+ $this->dimensionOrder[$pos++ + $diff] = $name;
+ }
+ } else {
+ $this->dimensionOrder = array_keys($this->dimensions);
+ }
+
+ $this->reversedDimensions = array_reverse($this->dimensionOrder);
+ $this->dimensionLevels = array_flip($this->dimensionOrder);
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeFacts()
+ {
+ $this->facts = $this->cube->listFacts();
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeSummaries()
+ {
+ $this->summaries = (object) array();
+ return $this;
+ }
+
+ /**
+ * @param object $row
+ * @return bool
+ */
+ protected function startsDimension($row)
+ {
+ foreach ($this->dimensionOrder as $name) {
+ if ($row->$name === null) {
+ $this->summaries->$name = $this->extractFacts($row);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $row
+ * @return object
+ */
+ protected function extractFacts($row)
+ {
+ $res = (object) array();
+
+ foreach ($this->facts as $fact) {
+ $res->$fact = $row->$fact;
+ }
+
+ return $res;
+ }
+
+ public function render(View $view)
+ {
+ $this->view = $view;
+ $this->initialize();
+ $htm = $this->beginContainer();
+
+ $results = $this->cube->fetchAll();
+
+ if (! empty($results) && $this->cube::isUsingIcingaDb()) {
+ $sortBy = $this->cube->getSortBy();
+ if ($sortBy && $sortBy[0] === $this->cube::DIMENSION_SEVERITY_SORT_PARAM) {
+ $isSortDirDesc = isset($sortBy[1]) && $sortBy[1] !== 'asc';
+ $results = $this->sortBySeverity($results, $isSortDirDesc);
+ }
+ }
+
+ foreach ($results as $row) {
+ $htm .= $this->renderRow($row);
+ }
+
+ return $htm . $this->closeDimensions() . $this->endContainer();
+ }
+
+
+ /**
+ * Sort the results by severity
+ *
+ * @param $results array The fetched results
+ * @param $isSortDirDesc bool Whether the sort direction is descending
+ *
+ * @return Generator
+ */
+ private function sortBySeverity(array $results, bool $isSortDirDesc): Generator
+ {
+ $perspective = end($this->dimensionOrder);
+ $resultsCount = count($results);
+ $tree = [new TreeNode()];
+
+ $prepareHeaders = function (array $tree, object $row): TreeNode {
+ $node = (new TreeNode())
+ ->setValue($row);
+ $parent = end($tree);
+ $parent->appendChild($node);
+
+ return $node;
+ };
+
+ $i = 0;
+ do {
+ $row = $results[$i];
+ while ($row->$perspective === null) {
+ $tree[] = $prepareHeaders($tree, $row);
+
+ if (! isset($results[++$i])) {
+ break;
+ }
+
+ $row = $results[$i];
+ }
+
+ for (; $i < $resultsCount; $i++) {
+ $row = $results[$i];
+
+ $anyNull = false;
+ foreach ($this->dimensionOrder as $dimension) {
+ if ($row->$dimension === null) {
+ $anyNull = true;
+ array_pop($tree);
+ }
+ }
+
+ if ($anyNull) {
+ break;
+ }
+
+ $prepareHeaders($tree, $row);
+ }
+ } while ($i < $resultsCount);
+
+ $nodes = function (TreeNode $node) use (&$nodes, $isSortDirDesc): Generator {
+ yield $node->getValue();
+ $children = $node->getChildren();
+
+ uasort($children, function (TreeNode $a, TreeNode $b) use ($isSortDirDesc): int {
+ foreach ($this->getSeveritySortColumns() as $column) {
+ $comparison = $a->getValue()->$column <=> $b->getValue()->$column;
+ if ($comparison !== 0) {
+ return $comparison * ($isSortDirDesc ? -1 : 1);
+ }
+ }
+
+ // $a and $b are equal in terms of $priorities.
+ return 0;
+ });
+
+ foreach ($children as $node) {
+ yield from $nodes($node);
+ }
+ };
+
+ return $nodes($tree[1]);
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = '';
+ if ($dimension = $this->startsDimension($row)) {
+ return $htm;
+ }
+
+ $htm .= $this->closeDimensionsForRow($row);
+ $htm .= $this->beginDimensionsForRow($row);
+ $htm .= $this->renderFacts($row);
+ $this->lastRow = $row;
+ return $htm;
+ }
+
+ protected function beginDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->beginDimensionsUpFrom($name, $row);
+ }
+ }
+
+ return '';
+ }
+
+ protected function beginDimensionsUpFrom($dimension, $row)
+ {
+ $htm = '';
+ $found = false;
+
+ foreach ($this->dimensionOrder as $name) {
+ if ($name === $dimension) {
+ $found = true;
+ }
+
+ if ($found) {
+ $htm .= $this->beginDimension($name, $row);
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->closeDimensionsDownTo($name);
+ }
+ }
+
+ return '';
+ }
+
+ protected function closeDimensionsDownTo($name)
+ {
+ $htm = '';
+
+ foreach ($this->reversedDimensions as $dimension) {
+ $htm .= $this->closeDimension($dimension);
+
+ if ($name === $dimension) {
+ break;
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensions()
+ {
+ $htm = '';
+ foreach ($this->reversedDimensions as $name) {
+ $htm .= $this->closeDimension($name);
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimension($name)
+ {
+ if (! $this->started) {
+ return '';
+ }
+
+ $indent = $this->getIndent($name);
+ return $indent . ' </div>' . "\n" . $indent . "</div><!-- $name -->\n";
+ }
+
+ protected function getIndent($name)
+ {
+ return str_repeat(' ', $this->getLevel($name));
+ }
+
+ protected function beginDimension($name, $row)
+ {
+ $indent = $this->getIndent($name);
+ if (! $this->started) {
+ $this->started = true;
+ }
+ $view = $this->view;
+ $dimension = $this->cube->getDimension($name);
+
+ return
+ $indent . '<div class="'
+ . $this->getDimensionClassString($name, $row)
+ . '">' . "\n"
+ . $indent . ' <div class="header"><a href="'
+ . $this->getDetailsUrl($name, $row)
+ . '" title="' . $view->escape(sprintf('Show details for %s: %s', $dimension->getLabel(), $row->$name)) . '"'
+ . ' data-base-target="_next">'
+ . $this->renderDimensionLabel($name, $row)
+ . '</a><a class="icon-filter" href="'
+ . $this->getSliceUrl($name, $row)
+ . '" title="' . $view->escape('Slice this cube') . '"></a></div>' . "\n"
+ . $indent . ' <div class="body">' . "\n";
+ }
+
+ /**
+ * Render the label for a given dimension name
+ *
+ * To have some context available, also
+ *
+ * @param $name
+ * @param $row
+ * @return string
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $caption = $row->$name;
+ if (empty($caption)) {
+ $caption = '_';
+ }
+
+ return $this->view->escape($caption);
+ }
+
+ protected function getDetailsUrl($name, $row)
+ {
+ $url = Url::fromPath($this->getDetailsBaseUrl());
+
+ if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) {
+ /** @var Filter\Rule $baseFilter */
+ $baseFilter = $this->cube->getBaseFilter();
+ $url->setFilter($baseFilter);
+ }
+
+ $urlParams = $url->getParams();
+
+ $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices());
+ $urlParams->add('dimensions', DimensionParams::update($dimensions)->getParams());
+
+ foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) {
+ $urlParams->add($this->cube::SLICE_PREFIX . $dimensionName, $row->$dimensionName);
+ }
+
+ foreach ($this->cube->getSlices() as $key => $val) {
+ $urlParams->add($this->cube::SLICE_PREFIX . $key, $val);
+ }
+
+ return $url;
+ }
+
+ protected function getSliceUrl($name, $row)
+ {
+ return $this->view->url()
+ ->setParam($this->cube::SLICE_PREFIX . $name, $row->$name);
+ }
+
+ protected function isOuterDimension($name)
+ {
+ return $this->reversedDimensions[0] !== $name;
+ }
+
+ protected function getDimensionClassString($name, $row)
+ {
+ return implode(' ', $this->getDimensionClasses($name, $row));
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ return array('cube-dimension' . $this->getLevel($name));
+ }
+
+ protected function getLevel($name)
+ {
+ return $this->dimensionLevels[$name];
+ }
+
+ /**
+ * @return string
+ */
+ protected function beginContainer()
+ {
+ return '<div class="cube">' . "\n";
+ }
+
+ /**
+ * @return string
+ */
+ protected function endContainer()
+ {
+ return '</div>' . "\n";
+ }
+
+ /**
+ * Well... just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->cube);
+ }
+}
diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
new file mode 100644
index 0000000..777f41b
--- /dev/null
+++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
@@ -0,0 +1,143 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Generator;
+use Icinga\Module\Cube\CubeRenderer;
+
+class HostStatusCubeRenderer extends CubeRenderer
+{
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->hosts_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+ $sums = $row;
+
+ $next = $this->cube->getDimensionAfter($name);
+ if ($next && isset($this->summaries->{$next->getName()})) {
+ $sums = $this->summaries->{$next->getName()};
+ }
+
+ $severityClass = [];
+ if ($sums->hosts_unhandled_down > 0) {
+ $severityClass[] = 'critical';
+ } elseif (isset($sums->hosts_unhandled_unreachable) && $sums->hosts_unhandled_unreachable > 0) {
+ $severityClass[] = 'unreachable';
+ }
+
+ if (empty($severityClass)) {
+ if ($sums->hosts_down > 0) {
+ $severityClass = ['critical', 'handled'];
+ } elseif (isset($sums->hosts_unreachable) && $sums->hosts_unreachable > 0) {
+ $severityClass = ['unreachable', 'handled'];
+ } else {
+ $severityClass[] = 'ok';
+ }
+ }
+
+ return array_merge($classes, $severityClass);
+ }
+
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = array();
+
+ if ($facts->hosts_unhandled_down > 0) {
+ $parts['critical'] = $facts->hosts_unhandled_down;
+ }
+
+ if (isset($facts->hosts_unhandled_unreachable) && $facts->hosts_unhandled_unreachable > 0) {
+ $parts['unreachable'] = $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) {
+ $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down;
+ }
+
+ if (
+ isset($facts->hosts_unreachable, $facts->hosts_unhandled_unreachable)
+ && $facts->hosts_unreachable > 0
+ && $facts->hosts_unreachable >
+ $facts->hosts_unhandled_unreachable
+ ) {
+ $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable;
+ }
+
+ if (
+ $facts->hosts_cnt > $facts->hosts_down
+ && (! isset($facts->hosts_unreachable) || $facts->hosts_cnt > $facts->hosts_unreachable)
+ ) {
+ $ok = $facts->hosts_cnt - $facts->hosts_down;
+ if (isset($facts->hosts_unreachable)) {
+ $ok -= $facts->hosts_unreachable;
+ }
+
+ $parts['ok'] = $ok;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($count === 0) {
+ continue;
+ }
+
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/hosts/details';
+ }
+
+ protected function getSeveritySortColumns(): Generator
+ {
+ $columns = ['down', 'unreachable'];
+ foreach ($columns as $column) {
+ yield "hosts_unhandled_$column";
+ }
+
+ foreach ($columns as $column) {
+ yield "hosts_$column";
+ }
+ }
+}
diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
new file mode 100644
index 0000000..f115742
--- /dev/null
+++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
@@ -0,0 +1,149 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Generator;
+use Icinga\Module\Cube\CubeRenderer;
+
+class ServiceStatusCubeRenderer extends CubeRenderer
+{
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = [];
+
+ if ($facts->services_unhandled_critical > 0) {
+ $parts['critical'] = $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unhandled_unknown > 0) {
+ $parts['unknown'] = $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_unhandled_warning > 0) {
+ $parts['warning'] = $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) {
+ $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) {
+ $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) {
+ $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning;
+ }
+
+ if (
+ $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning
+ && $facts->services_cnt > $facts->services_unknown
+ ) {
+ $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning -
+ $facts->services_unknown;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($count === 0) {
+ continue;
+ }
+
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->services_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+ $sums = $row;
+
+ $next = $this->cube->getDimensionAfter($name);
+ if ($next && isset($this->summaries->{$next->getName()})) {
+ $sums = $this->summaries->{$next->getName()};
+ }
+
+ if ($sums->services_unhandled_critical > 0) {
+ $severityClass[] = 'critical';
+ } elseif ($sums->services_unhandled_unknown > 0) {
+ $severityClass[] = 'unknown';
+ } elseif ($sums->services_unhandled_warning > 0) {
+ $severityClass[] = 'warning';
+ }
+
+ if (empty($severityClass)) {
+ if ($sums->services_critical > 0) {
+ $severityClass = ['critical', 'handled'];
+ } elseif ($sums->services_unknown > 0) {
+ $severityClass = ['unknown', 'handled'];
+ } elseif ($sums->services_warning > 0) {
+ $severityClass = ['warning', 'handled'];
+ } else {
+ $severityClass[] = 'ok';
+ }
+ }
+
+ return array_merge($classes, $severityClass);
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/services/details';
+ }
+
+ protected function getSeveritySortColumns(): Generator
+ {
+ $columns = ['critical', 'unknown', 'warning'];
+ foreach ($columns as $column) {
+ yield "services_unhandled_$column";
+ }
+
+ foreach ($columns as $column) {
+ yield "services_$column";
+ }
+ }
+}
diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php
new file mode 100644
index 0000000..071e934
--- /dev/null
+++ b/library/Cube/Dimension.php
@@ -0,0 +1,70 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+/**
+ * Dimension interface
+ *
+ * All available dimensions must implement this interface
+ *
+ * @package Icinga\Module\Cube
+ */
+interface Dimension
+{
+ /**
+ * The name of this dimension
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Fetch label of this dimension
+ *
+ * @return string
+ */
+ public function getLabel();
+
+ /**
+ * Add a label
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function addLabel(string $label);
+
+ /**
+ * Set the label for the dimension
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setLabel(string $label);
+
+ /**
+ * Column expression
+ *
+ * This is the expression used to fetch the related column. Usually an SQL
+ * snippet when a relational database is involved
+ *
+ * @param Cube $cube
+ *
+ * @return string
+ */
+ public function getColumnExpression(Cube $cube);
+
+ /**
+ * Add this dimension to a cube
+ *
+ * This allows your dimension to apply itself to the Cube. That way your
+ * dimension is able to join optional tables and more
+ *
+ * @param Cube $cube
+ * @return void
+ */
+ public function addToCube(Cube $cube);
+}
diff --git a/library/Cube/DimensionParams.php b/library/Cube/DimensionParams.php
new file mode 100644
index 0000000..c0205bb
--- /dev/null
+++ b/library/Cube/DimensionParams.php
@@ -0,0 +1,85 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Web\Url;
+use ipl\Stdlib\Str;
+
+class DimensionParams
+{
+ /**
+ * @var array Raw dimensions
+ */
+ protected $dimensions = [];
+
+ /**
+ * @var string encoded dimensions separated by coma
+ */
+ protected $params;
+
+ public static function fromUrl(Url $url)
+ {
+ return static::fromString($url->getParam('dimensions'));
+ }
+
+ public static function fromArray(array $dimensions = [])
+ {
+ $self = new static();
+
+ $self->dimensions = array_filter($dimensions);
+
+ return $self;
+ }
+
+ public static function fromString($dimensions)
+ {
+ return static::fromArray(Str::trimSplit($dimensions));
+ }
+
+ /**
+ * @param $dimension
+ *
+ * @return $this
+ */
+ public function add($dimension)
+ {
+ if (! empty($dimension)) {
+ $this->dimensions[] = $dimension;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Overwrite dimensions
+ *
+ * @param $dimensions
+ *
+ * @return DimensionParams
+ */
+ public static function update($dimensions)
+ {
+ $self = new static();
+ $self->dimensions = $dimensions;
+
+ return $self;
+ }
+
+ /**
+ * @return string encoded dimensions separated by coma
+ */
+ public function getParams()
+ {
+ return implode(',', $this->dimensions);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDimensions()
+ {
+ return $this->dimensions;
+ }
+}
diff --git a/library/Cube/Hook/ActionsHook.php b/library/Cube/Hook/ActionsHook.php
new file mode 100644
index 0000000..8ba8a7c
--- /dev/null
+++ b/library/Cube/Hook/ActionsHook.php
@@ -0,0 +1,99 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Web\ActionLink;
+use Icinga\Module\Cube\Web\ActionLinks;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class ActionsHook
+{
+ /** @var ActionLinks */
+ private $actionLinks;
+
+ /**
+ * Your implementation should extend this method
+ *
+ * Then use the addActionLink() method, eventually combined with the
+ * createUrl() helper like this:
+ *
+ * <code>
+ * $this->addActionLink(
+ * $this->makeUrl('mymodule/controller/action', array('some' => 'param')),
+ * 'A shown title',
+ * 'A longer description text, should fit into the available square field',
+ * 'icon-name'
+ * );
+ * </code>
+ *
+ * For a list of available icon names please enable the Icinga Web 2 'doc'
+ * module and go to "Documentation" -> "Developer - Style" -> "Icons"
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return void
+ */
+ abstract public function prepareActionLinks(Cube $cube, View $view);
+
+ /**
+ * Lazy access to an ActionLinks object
+ *
+ * @return ActionLinks
+ */
+ public function getActionLinks()
+ {
+ if ($this->actionLinks === null) {
+ $this->actionLinks = new ActionLinks();
+ }
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method instantiating an ActionLink object
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function addActionLink(Url $url, $title, $description, $icon)
+ {
+ $this->getActionLinks()->add(
+ new ActionLink($url, $title, $description, $icon)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ public function makeUrl($path, $params = null)
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+}
diff --git a/library/Cube/Hook/IcingaDbActionsHook.php b/library/Cube/Hook/IcingaDbActionsHook.php
new file mode 100644
index 0000000..63c24fe
--- /dev/null
+++ b/library/Cube/Hook/IcingaDbActionsHook.php
@@ -0,0 +1,125 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use ipl\Web\Url;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class IcingaDbActionsHook
+{
+ /** @var Link[] */
+ private $actionLinks = [];
+
+ /**
+ * Create additional action links for the given cube
+ *
+ * @param IcingaDbCube $cube
+ * @return void
+ */
+ abstract public function createActionLinks(IcingaDbCube $cube);
+
+ /**
+ * Return the action links for the cube
+ *
+ * @return Link[]
+ */
+ final protected function getActionLinks(): array
+ {
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method to populate action links array
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ final protected function addActionLink(Url $url, string $title, string $description, string $icon): self
+ {
+ $linkContent = (new HtmlDocument());
+ $linkContent->addHtml(new Icon($icon));
+ $linkContent->addHtml(HtmlElement::create('span', ['class' => 'title'], $title));
+ $linkContent->addHtml(HtmlElement::create('p', null, $description));
+
+ $this->actionLinks[] = new Link($linkContent, $url);
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ final protected function makeUrl(string $path, array $params = null): Url
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Render all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param IcingaDbCube $cube
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube)
+ {
+ $html = new HtmlDocument();
+
+ /** @var IcingaDbActionsHook $hook */
+ foreach (Hook::all('Cube/IcingaDbActions') as $hook) {
+ try {
+ $hook->createActionLinks($cube);
+ } catch (Exception $e) {
+ $html->addHtml(HtmlElement::create('li', ['class' => 'error'], $e->getMessage()));
+ }
+
+ foreach ($hook->getActionLinks() as $link) {
+ $html->addHtml(HtmlElement::create('li', null, $link));
+ }
+ }
+
+ if ($html->isEmpty()) {
+ $html->addHtml(
+ HtmlElement::create(
+ 'li',
+ ['class' => 'error'],
+ t('No action links have been provided for this cube')
+ )
+ );
+ }
+
+ return $html->render();
+ }
+}
diff --git a/library/Cube/IcingaDb/CustomVariableDimension.php b/library/Cube/IcingaDb/CustomVariableDimension.php
new file mode 100644
index 0000000..34a395c
--- /dev/null
+++ b/library/Cube/IcingaDb/CustomVariableDimension.php
@@ -0,0 +1,166 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter;
+
+class CustomVariableDimension implements Dimension
+{
+ use Auth;
+
+ /** @var string Prefix for host custom variable */
+ public const HOST_PREFIX = 'host.vars.';
+
+ /** @var string Prefix for service custom variable */
+ public const SERVICE_PREFIX = 'service.vars.';
+
+ /** @var ?string variable source name */
+ protected $sourceName;
+
+ /** @var ?string Variable name without prefix */
+ protected $varName;
+
+ /** @var string Variable name with prefix */
+ protected $name;
+
+ protected $label;
+
+ protected $wantNull = false;
+
+ public function __construct($name)
+ {
+ if (preg_match('/^(host|service)\.vars\.(.*)/', $name, $matches)) {
+ $this->sourceName = $matches[1];
+ $this->varName = $matches[2];
+ }
+
+ $this->name = $name;
+ }
+
+ /**
+ * Get the variable name without prefix
+ *
+ * @return string
+ */
+ public function getVarName(): string
+ {
+ return $this->varName ?? $this->getName();
+ }
+
+ /**
+ * Get the variable source name
+ *
+ * @return ?string
+ */
+ public function getSourceName(): ?string
+ {
+ return $this->sourceName;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getLabel()
+ {
+ return $this->label ?: $this->getName();
+ }
+
+ public function setLabel($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ public function addLabel($label)
+ {
+ if ($this->label === null) {
+ $this->setLabel($label);
+ } else {
+ $this->label .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaDbCube $cube
+ * @return Expression|string
+ */
+ public function getColumnExpression(Cube $cube)
+ {
+ $expression = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias(), 'flatvalue']);
+
+ if ($this->wantNull) {
+ return new Expression("COALESCE($expression, '-')");
+ }
+
+ return $expression;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ /** @var IcingaDbCube $cube */
+ $innerQuery = $cube->innerQuery();
+ $sourceTable = $this->getSourceName() ?? $innerQuery->getModel()->getTableName();
+
+ $subQuery = $innerQuery->createSubQuery(new CustomvarFlat(), $sourceTable . '.vars');
+ $subQuery->getSelectBase()->resetWhere(); // The link to the outer query is the ON condition
+ $subQuery->columns(['flatvalue', 'object_id' => $sourceTable . '.id']);
+ $subQuery->filter(Filter::like('flatname', $this->getVarName()));
+
+ // Values might not be unique (wildcard dimensions)
+ $subQueryModelAlias = $subQuery->getResolver()->getAlias($subQuery->getModel());
+ $subQuery->getSelectBase()->groupBy([
+ $subQueryModelAlias . '.flatname', // Required by postgres, if there are any custom variable protections
+ $subQueryModelAlias . '.flatvalue',
+ 'object_id'
+ ]);
+
+ $this->applyRestrictions($subQuery);
+
+ $subQueryAlias = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias()]);
+ $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue');
+
+ $sourceIdPath = '.id';
+ if ($innerQuery->getModel() instanceof Service && $sourceTable === 'host') {
+ $sourceIdPath = '.host_id';
+ }
+
+ $innerQuery->getSelectBase()->join(
+ [$subQueryAlias => $subQuery->assembleSelect()],
+ [
+ $subQueryAlias . '.object_id = '
+ . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . $sourceIdPath
+ ]
+ );
+ }
+
+ protected function createCustomVarAlias(): string
+ {
+ return implode('_', ['c', $this->getSourceName(), $this->getVarName()]);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php
new file mode 100644
index 0000000..44c7619
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbCube.php
@@ -0,0 +1,338 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Orm\Common\SortUtil;
+use ipl\Orm\Query;
+use ipl\Sql\Adapter\Pgsql;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+use ipl\Stdlib\BaseFilter;
+
+abstract class IcingaDbCube extends Cube
+{
+ use Auth;
+ use BaseFilter;
+ use Database;
+
+ public const SLICE_PREFIX = 'slice.';
+ public const IS_USING_ICINGADB = true;
+
+ /** @var bool Whether to show problems only */
+ protected $problemsOnly = false;
+
+ /** @var string Sort param used to sort dimensions by value */
+ public const DIMENSION_VALUE_SORT_PARAM = 'value';
+
+ /** @var string Sort param used to sort dimensions by severity */
+ public const DIMENSION_SEVERITY_SORT_PARAM = 'severity';
+
+ /** @var Query The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ protected $objectsFilter;
+
+ /** @var array The sort order of dimensions, column as key and direction as value */
+ protected $sortBy;
+
+ abstract public function getObjectsFilter();
+ /**
+ * An IcingaDbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return Query
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return Query
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = $this->prepareInnerQuery();
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * Get our rollup query
+ *
+ * @return Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ $this->addDimension($this->createDimension($name));
+
+ return $this;
+ }
+
+ /**
+ * Set whether to show problems only
+ *
+ * @param bool $problemOnly
+ *
+ * @return $this
+ */
+ public function problemsOnly(bool $problemOnly = true): self
+ {
+ $this->problemsOnly = $problemOnly;
+
+ return $this;
+ }
+
+
+ /**
+ * Get whether to show problems only
+ *
+ * @return bool
+ */
+ public function isProblemsOnly(): bool
+ {
+ return $this->problemsOnly;
+ }
+
+ /**
+ * Fetch the host variable dimensions
+ *
+ * @return array
+ */
+ public function fetchHostVariableDimensions(): array
+ {
+ $query = Host::on($this->getDb())
+ ->with('customvar_flat')
+ ->columns('customvar_flat.flatname')
+ ->orderBy('customvar_flat.flatname');
+
+ $this->applyRestrictions($query);
+
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->customvar_flat->flatname);
+ $name = strtolower($name);
+ $dimensions[CustomVariableDimension::HOST_PREFIX . $name] = 'Host ' . $name;
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * Fetch the service variable dimensions
+ *
+ * @return array
+ */
+ public function fetchServiceVariableDimensions(): array
+ {
+ $query = Service::on($this->getDb())
+ ->with('customvar_flat')
+ ->columns('customvar_flat.flatname')
+ ->orderBy('customvar_flat.flatname');
+
+ $this->applyRestrictions($query);
+
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->customvar_flat->flatname);
+ $name = strtolower($name);
+ $dimensions[CustomVariableDimension::SERVICE_PREFIX . $name] = 'Service ' . $name;
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * Set sort by columns
+ *
+ * @param ?string $sortBy
+ *
+ * @return $this
+ */
+ public function sortBy(?string $sortBy): self
+ {
+ if (empty($sortBy)) {
+ return $this;
+ }
+
+ $this->sortBy = SortUtil::createOrderBy($sortBy)[0];
+
+ return $this;
+ }
+
+ /**
+ * Get sort by columns
+ *
+ * @return ?array Column as key and direction as value
+ */
+ public function getSortBy(): ?array
+ {
+ return $this->sortBy;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ protected function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery();
+ $select = $query->getSelectBase();
+
+ $columns = [];
+ foreach ($this->dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+ $dimension->addToCube($this);
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+
+ if ($this->hasSlice($name)) {
+ $select->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $select->columns($columns);
+
+ $this->applyRestrictions($query);
+ if ($this->hasBaseFilter()) {
+ $query->filter($this->getBaseFilter());
+ }
+ }
+
+ protected function prepareRollupQuery()
+ {
+ $dimensions = $this->listDimensions();
+ $this->finalizeInnerQuery();
+
+ $columns = [];
+ $groupBy = [];
+ foreach ($dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+
+ $columns[$quotedDimension] = 'f.' . $quotedDimension;
+ $groupBy[] = $quotedDimension;
+ }
+
+ $availableFacts = $this->getAvailableFactColumns();
+
+ foreach ($this->chosenFacts as $alias) {
+ $columns[$alias] = new Expression('SUM(f.' . $availableFacts[$alias] . ')');
+ }
+
+ if (! empty($groupBy)) {
+ if ($this->getDb()->getAdapter() instanceof Pgsql) {
+ $groupBy = 'ROLLUP(' . implode(', ', $groupBy) . ')';
+ } else {
+ $groupBy[count($groupBy) - 1] .= ' WITH ROLLUP';
+ }
+ }
+
+ $rollupQuery = new Select();
+ $rollupQuery->from(['f' => $this->innerQuery()->assembleSelect()])
+ ->columns($columns)
+ ->groupBy($groupBy);
+
+ return $rollupQuery;
+ }
+
+ protected function prepareFullQuery()
+ {
+ $rollupQuery = $this->rollupQuery();
+ $columns = [];
+ $orderBy = [];
+ $sortBy = $this->getSortBy();
+ foreach ($this->listColumns() as $column) {
+ $quotedColumn = $this->getDb()->quoteIdentifier([$column]);
+ $columns[$quotedColumn] = 'rollup.' . $quotedColumn;
+
+ if ($this->hasDimension($column)) {
+ $orderBy["($quotedColumn IS NOT NULL)"] = null;
+
+ $sortDir = 'ASC';
+ if ($sortBy && self::DIMENSION_VALUE_SORT_PARAM === $sortBy[0]) {
+ $sortDir = $sortBy[1] ?? 'ASC';
+ }
+
+ $orderBy[$quotedColumn] = $sortDir;
+ }
+ }
+
+ return (new Select())
+ ->from(['rollup' => $rollupQuery])
+ ->columns($columns)
+ ->orderBy($orderBy);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->getDb()->fetchAll($query);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
new file mode 100644
index 0000000..14e083f
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
@@ -0,0 +1,80 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+
+class IcingaDbHostStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'hosts_cnt' => 'hosts_total',
+ 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled',
+ 'hosts_unhandled_down' => 'hosts_down_unhandled',
+ ];
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function listAvailableDimensions()
+ {
+ return $this->fetchHostVariableDimensions();
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = HoststateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Host())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ $this->innerQuery = $query;
+ return $this->innerQuery;
+ }
+
+ /**
+ * Return Filter for Hosts cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $hosts = $this->innerQuery()->columns(['host' => 'host.name']);
+ $hosts->getSelectBase()->resetGroupBy();
+
+ $filter = Filter::any();
+
+ foreach ($hosts as $object) {
+ $filter->add(Filter::equal('host.name', $object->host));
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
new file mode 100644
index 0000000..ac59de2
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
@@ -0,0 +1,94 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+
+class IcingaDbServiceStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'services_total',
+ 'services_critical' => 'services_critical_handled + f.services_critical_unhandled',
+ 'services_unhandled_critical' => 'services_critical_unhandled',
+ 'services_warning' => 'services_warning_handled + f.services_warning_unhandled',
+ 'services_unhandled_warning' => 'services_warning_unhandled',
+ 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled',
+ 'services_unhandled_unknown' => 'services_unknown_unhandled',
+ ];
+ }
+
+ public function listAvailableDimensions()
+ {
+ return array_merge(
+ $this->fetchServiceVariableDimensions(),
+ $this->fetchHostVariableDimensions()
+ );
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = ServicestateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Service())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ return $query;
+ }
+
+ /**
+ * Return Filter for Services cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $services = $this->innerQuery()->columns([
+ 'host_name' => 'host.name',
+ 'service_name' => 'service.name'
+ ]);
+
+ $services->getSelectBase()->resetGroupBy();
+ $filter = Filter::any();
+
+ foreach ($services as $service) {
+ $filter->add(
+ Filter::all(
+ Filter::equal('service.name', $service->service_name),
+ Filter::equal('host.name', $service->host_name)
+ )
+ );
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/Ido/CustomVarDimension.php b/library/Cube/Ido/CustomVarDimension.php
new file mode 100644
index 0000000..df45497
--- /dev/null
+++ b/library/Cube/Ido/CustomVarDimension.php
@@ -0,0 +1,146 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+
+/**
+ * CustomVarDimension
+ *
+ * This provides dimenstions for custom variables available in the IDO
+ *
+ * TODO: create safe aliases for special characters
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+class CustomVarDimension implements Dimension
+{
+ public const TYPE_HOST = 'host';
+
+ public const TYPE_SERVICE = 'service';
+
+ /**
+ * @var string custom variable name
+ */
+ protected $varName;
+
+ /**
+ * @var string custom variable label
+ */
+ protected $varLabel;
+
+ /**
+ * @var bool Whether null values should be shown
+ */
+ protected $wantNull = false;
+
+ /** @var string Type of the custom var */
+ protected $type;
+
+ /**
+ * CustomVarDimension constructor.
+ *
+ * @param $varName
+ * @param string $type Type of the custom var
+ */
+ public function __construct($varName, $type = null)
+ {
+ $this->varName = $varName;
+ $this->type = $type;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->varName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->varLabel ?: $this->getName();
+ }
+
+ public function setLabel($label)
+ {
+ $this->varLabel = $label;
+
+ return $this;
+ }
+
+ public function addLabel($label)
+ {
+ if ($this->varLabel === null) {
+ $this->setLabel($label);
+ } else {
+ $this->varLabel .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ public function getColumnExpression(Cube $cube)
+ {
+ /** @var IdoCube $cube */
+ if ($this->wantNull) {
+ return 'COALESCE(' . $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']) . ", '-')";
+ } else {
+ return $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']);
+ }
+ }
+
+ protected function safeVarname($name)
+ {
+ return $name;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ switch ($this->type) {
+ case self::TYPE_HOST:
+ $objectId = 'ho.object_id';
+ break;
+ case self::TYPE_SERVICE:
+ $objectId = 'so.object_id';
+ break;
+ default:
+ $objectId = 'o.object_id';
+ }
+ $name = $this->safeVarname($this->varName);
+ /** @var IdoCube $cube */
+ $alias = $cube->db()->quoteIdentifier(['c_' . $name]);
+
+ if ($cube->isPgsql()) {
+ $on = "LOWER($alias.varname) = ?";
+ $name = strtolower($name);
+ } else {
+ $on = $alias . '.varname = ? COLLATE latin1_general_ci';
+ }
+
+ $cube->innerQuery()->joinLeft(
+ array($alias => $cube->tableName('icinga_customvariablestatus')),
+ $cube->db()->quoteInto($on, $name)
+ . ' AND ' . $alias . '.object_id = ' . $objectId,
+ array()
+ )->group($alias . '.varvalue');
+ }
+}
diff --git a/library/Cube/Ido/DataView/Hoststatus.php b/library/Cube/Ido/DataView/Hoststatus.php
new file mode 100644
index 0000000..32ee44b
--- /dev/null
+++ b/library/Cube/Ido/DataView/Hoststatus.php
@@ -0,0 +1,17 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\DataView;
+
+use Icinga\Data\ConnectionInterface;
+use Icinga\Module\Cube\Ido\Query\HoststatusQuery;
+
+class Hoststatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
+{
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ $this->connection = $connection;
+ $this->query = new HoststatusQuery($connection->getResource(), $columns);
+ }
+}
diff --git a/library/Cube/Ido/DbCube.php b/library/Cube/Ido/DbCube.php
new file mode 100644
index 0000000..5fc5b47
--- /dev/null
+++ b/library/Cube/Ido/DbCube.php
@@ -0,0 +1,298 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Cube\Cube;
+
+abstract class DbCube extends Cube
+{
+ /** @var DbConnection */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var ZfSelectWrapper The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var \Zend_Db_Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var \Zend_Db_Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ /** @var string Database name. Allows to eventually join over multiple dbs */
+ protected $dbName;
+
+ /** @var array Key/value array containing our chosen facts and the corresponding SQL expression */
+ protected $factColumns = array();
+
+ /**
+ * A DbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return \Zend_Db_Select
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Set a database connection
+ *
+ * @param DbConnection $connection
+ * @return $this
+ */
+ public function setConnection(DbConnection $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ return $this;
+ }
+
+ /**
+ * Prepare the query and fetch all data
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->db()->fetchAll($query);
+ }
+
+ /**
+ * Choose a one or more facts
+ *
+ * This also initializes a fact column lookup array
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ parent::chooseFacts($facts);
+
+ $this->factColumns = array();
+ $columns = $this->getAvailableFactColumns();
+ foreach ($this->chosenFacts as $name) {
+ $this->factColumns[$name] = $columns[$name];
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ * @return $this
+ */
+ public function setDbName($name)
+ {
+ $this->dbName = $name;
+ return $this;
+ }
+
+ /**
+ * Gives back the table name, eventually prefixed with a defined DB name
+ *
+ * @param string $name
+ * @return string
+ */
+ public function tableName($name)
+ {
+ if ($this->dbName === null) {
+ return $name;
+ } else {
+ return $this->dbName . '.' . $name;
+ }
+ }
+
+ /**
+ * Returns an eventually defined DB name
+ *
+ * @return string|null
+ */
+ public function getDbName()
+ {
+ return $this->dbName;
+ }
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return ZfSelectWrapper
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = new ZfSelectWrapper($this->prepareInnerQuery());
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ public function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery()->unwrap();
+ $columns = array();
+ foreach ($this->dimensions as $name => $dimension) {
+ $dimension->addToCube($this);
+ if ($this->hasSlice($name)) {
+ $query->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$name] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $c = [];
+
+ foreach ($columns + $this->factColumns as $k => $v) {
+ $c[$this->db()->quoteIdentifier([$k])] = $v;
+ }
+
+ $query->columns($c);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * The full query wraps the rollup query in a sub-query to work around
+ * MySQL limitations. This is required to not get into trouble when ordering,
+ * especially combined with the need to keep control over (eventually desired)
+ * NULL value fact columns
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareFullQuery()
+ {
+ $alias = 'rollup';
+ $cols = $this->listColumns();
+
+ $columns = array();
+
+ foreach ($cols as $col) {
+ $columns[$this->db()->quoteIdentifier([$col])] = $alias . '.' . $this->db()->quoteIdentifier([$col]);
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->rollupQuery()),
+ $columns
+ );
+
+ foreach ($columns as $col) {
+ $select->order('(' . $col . ' IS NOT NULL)');
+ $select->order($col);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Provide access to our DB
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ return $this->db;
+ }
+
+ /**
+ * Whether our connection is PostgreSQL
+ *
+ * @return bool
+ */
+ public function isPgsql()
+ {
+ return $this->connection->getDbType() === 'pgsql';
+ }
+
+
+ /**
+ * This prepares the rollup query
+ *
+ * Inner query is wrapped in a subquery, summaries for all facts are
+ * fetched. Rollup considers all defined dimensions and expects them
+ * to exist as columns in the innerQuery
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareRollupQuery()
+ {
+ $alias = 'sub';
+
+ $dimensions = array_map(function ($val) {
+ return $this->db()->quoteIdentifier([$val]);
+ }, array_keys($this->listDimensions()));
+ $this->finalizeInnerQuery();
+ $columns = array();
+ foreach ($dimensions as $dimension) {
+ $columns[$dimension] = $alias . '.' . $dimension;
+ }
+
+ foreach ($this->listFacts() as $fact) {
+ $columns[$fact] = 'SUM(' . $fact . ')';
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->innerQuery()->unwrap()),
+ $columns
+ );
+
+ if ($this->isPgsql()) {
+ $select->group('ROLLUP (' . implode(', ', $dimensions) . ')');
+ } else {
+ $select->group('(' . implode('), (', $dimensions) . ') WITH ROLLUP');
+ }
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoCube.php b/library/Cube/Ido/IdoCube.php
new file mode 100644
index 0000000..7ad609d
--- /dev/null
+++ b/library/Cube/Ido/IdoCube.php
@@ -0,0 +1,219 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Util\GlobFilter;
+
+/**
+ * IdoCube
+ *
+ * Base class for IDO-related cubes
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+abstract class IdoCube extends DbCube
+{
+ /** @var array */
+ protected $availableFacts = array();
+
+ /** @var string We ask for the IDO version for compatibility reasons */
+ protected $idoVersion;
+
+ /** @var MonitoringBackend */
+ protected $backend;
+
+ /**
+ * Cache for {@link filterProtectedCustomvars()}
+ *
+ * @var string|null
+ */
+ protected $protectedCustomvars;
+
+ /** @var GlobFilter The properties to hide from the user */
+ protected $blacklistedProperties;
+
+ public const IS_USING_ICINGADB = false;
+
+ /**
+ * Add a specific named dimension
+ *
+ * Right now these are just custom vars, we might support group memberships
+ * or other properties in future
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function addDimensionByName($name): self
+ {
+ if (count($this->filterProtectedCustomvars([$name])) === 1) {
+ $this->addDimension($this->createDimension($name));
+ }
+
+ return $this;
+ }
+
+ /**
+ * We can steal the DB connection directly from a Monitoring backend
+ *
+ * @param MonitoringBackend $backend
+ * @return $this
+ */
+ public function setBackend(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+
+ $resource = $backend->getResource();
+ $resource->getDbAdapter()
+ ->getConnection()
+ ->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_NATURAL);
+
+ $this->setConnection($resource);
+
+ return $this;
+ }
+
+ /**
+ * Provice access to our DB resource
+ *
+ * This lazy-loads the default monitoring backend in case no DB has been
+ * given
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ $this->requireBackend();
+ return parent::db();
+ }
+
+ /**
+ * Returns the Icinga IDO version
+ *
+ * @return string
+ */
+ protected function getIdoVersion()
+ {
+ if ($this->idoVersion === null) {
+ $db = $this->db();
+ $this->idoVersion = $db->fetchOne(
+ $db->select()->from('icinga_dbversion', 'version')
+ );
+ }
+
+ return $this->idoVersion;
+ }
+
+ /**
+ * Steal the default monitoring DB resource...
+ *
+ * ...in case none has been defined otherwise
+ *
+ * @return void
+ */
+ protected function requireBackend()
+ {
+ if ($this->db === null) {
+ $this->setBackend(MonitoringBackend::instance());
+ }
+ }
+
+ protected function getMonitoringRestriction()
+ {
+ $restriction = Filter::matchAny();
+ $restriction->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/i', $c);
+ }
+ ));
+
+ $filters = Auth::getInstance()->getUser()->getRestrictions('monitoring/filter/objects');
+
+ foreach ($filters as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAny();
+ }
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s',
+ 'monitoring/filter/objects',
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ return $restriction;
+ }
+
+ /**
+ * Return the given array without values matching the custom variables protected by the monitoring module
+ *
+ * @param string[] $customvars
+ *
+ * @return string[]
+ */
+ protected function filterProtectedCustomvars(array $customvars)
+ {
+ if ($this->blacklistedProperties === null) {
+ $this->blacklistedProperties = new GlobFilter(
+ Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
+ );
+ }
+
+ if ($this instanceof IdoServiceStatusCube) {
+ $type = 'service';
+ } else {
+ $type = 'host';
+ }
+
+ $customvars = $this->blacklistedProperties->removeMatching(
+ [$type => ['vars' => array_flip($customvars)]]
+ );
+
+ $customvars = isset($customvars[$type]['vars']) ? array_flip($customvars[$type]['vars']) : [];
+
+ if ($this->protectedCustomvars === null) {
+ $config = Config::module('monitoring')->get('security', 'protected_customvars');
+ $protectedCustomvars = array();
+
+ foreach (preg_split('~,~', $config, -1, PREG_SPLIT_NO_EMPTY) as $pattern) {
+ $regex = array();
+ foreach (explode('*', $pattern) as $literal) {
+ $regex[] = preg_quote($literal, '/');
+ }
+
+ $protectedCustomvars[] = implode('.*', $regex);
+ }
+
+ $this->protectedCustomvars = empty($protectedCustomvars)
+ ? '/^$/'
+ : '/^(?:' . implode('|', $protectedCustomvars) . ')$/';
+ }
+
+ return preg_grep($this->protectedCustomvars, $customvars, PREG_GREP_INVERT);
+ }
+}
diff --git a/library/Cube/Ido/IdoHostStatusCube.php b/library/Cube/Ido/IdoHostStatusCube.php
new file mode 100644
index 0000000..3f79dc8
--- /dev/null
+++ b/library/Cube/Ido/IdoHostStatusCube.php
@@ -0,0 +1,97 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Cube\Ido\DataView\Hoststatus;
+
+class IdoHostStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAvailableFactColumns()
+ {
+ return array(
+ 'hosts_cnt' => 'SUM(CASE WHEN hs.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN hs.current_state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_unreachable' => 'SUM(CASE WHEN hs.current_state = 2'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ );
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_HOST);
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('hoststatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = ho.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ $dimensions = $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ $keys = array_map('strtolower', $dimensions);
+
+ return array_combine($keys, $dimensions);
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = new Hoststatus($this->backend);
+
+ $view->getQuery()->requireColumn('host_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoServiceStatusCube.php b/library/Cube/Ido/IdoServiceStatusCube.php
new file mode 100644
index 0000000..a403645
--- /dev/null
+++ b/library/Cube/Ido/IdoServiceStatusCube.php
@@ -0,0 +1,97 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+
+class IdoServiceStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'SUM(CASE WHEN ss.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unhandled_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
+ 'services_unhandled_warning' => 'SUM(CASE WHEN ss.current_state = 1'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
+ 'services_unhandled_unknown' => 'SUM(CASE WHEN ss.current_state = 3'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ ];
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = so.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ $dimensions = $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ $keys = array_map('strtolower', $dimensions);
+
+ return array_combine($keys, $dimensions);
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->getQuery()->requireColumn('service_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_SERVICE);
+ }
+}
diff --git a/library/Cube/Ido/Query/HoststatusQuery.php b/library/Cube/Ido/Query/HoststatusQuery.php
new file mode 100644
index 0000000..6a9aa96
--- /dev/null
+++ b/library/Cube/Ido/Query/HoststatusQuery.php
@@ -0,0 +1,47 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\Query;
+
+use Exception;
+use Icinga\Application\Version;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
+
+class HoststatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
+{
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup',
+ 'services' => 'servicestatus'
+ );
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'servicestatus') {
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+
+ protected function createSubQueryFilter(FilterExpression $filter, $queryName)
+ {
+ try {
+ return parent::createSubQueryFilter($filter, $queryName);
+ } catch (Exception $e) {
+ if (version_compare(Version::VERSION, '2.10.0', '>=')) {
+ throw $e;
+ }
+
+ if ($e->getMessage() === 'Undefined array key 0' && basename($e->getFile()) === 'IdoQuery.php') {
+ // Ensures compatibility with earlier Icinga Web 2 versions
+ throw new NotImplementedError('');
+ } else {
+ throw $e;
+ }
+ }
+ }
+}
diff --git a/library/Cube/Ido/ZfSelectWrapper.php b/library/Cube/Ido/ZfSelectWrapper.php
new file mode 100644
index 0000000..745d7a5
--- /dev/null
+++ b/library/Cube/Ido/ZfSelectWrapper.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Cube\Ido;
+
+/**
+ * Since version 1.1.0 we're using the monitoring module's queries as the cubes' base queries.
+ * Before, the host object table was available using the alias 'o'. Now it's 'ho'.
+ * Without this wrapper, the action link hook provided by the director would fail because it relies on the alias 'o'.
+ */
+class ZfSelectWrapper
+{
+ /** @var \Zend_Db_Select */
+ protected $select;
+
+ public function __construct(\Zend_Db_Select $select)
+ {
+ $this->select = $select;
+ }
+
+ /**
+ * Get the underlying Zend_Db_Select query
+ *
+ * @return \Zend_Db_Select
+ */
+ public function unwrap()
+ {
+ return $this->select;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::reset()}
+ */
+ public function reset($part = null)
+ {
+ $this->select->reset($part);
+
+ return $this;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::columns()}
+ */
+ public function columns($cols = '*', $correlationName = null)
+ {
+ if (is_array($cols)) {
+ foreach ($cols as $alias => &$col) {
+ if (substr($col, 0, 2) === 'o.') {
+ $col = 'ho.' . substr($col, 2);
+ }
+ }
+ }
+
+ return $this->select->columns($cols, $correlationName);
+ }
+
+ /**
+ * Proxy Zend_Db_Select method calls
+ *
+ * @param string $name The name of the method to call
+ * @param array $arguments Arguments for the method to call
+ *
+ * @return mixed
+ *
+ * @throws \BadMethodCallException If the called method does not exist
+ */
+ public function __call($name, array $arguments)
+ {
+ if (! method_exists($this->select, $name)) {
+ $class = get_class($this);
+ $message = "Call to undefined method $class::$name";
+
+ throw new \BadMethodCallException($message);
+ }
+
+ return call_user_func_array([$this->select, $name], $arguments);
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/IcingaDbActions.php b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
new file mode 100644
index 0000000..1fbd05d
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbServiceStatusCube;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+
+class IcingaDbActions extends IcingaDbActionsHook
+{
+ public function createActionLinks(IcingaDbCube $cube)
+ {
+ $type = 'host';
+ if ($cube instanceof IcingaDbServiceStatusCube) {
+ $type = 'service';
+ }
+
+ $filter = Filter::all();
+ if ($cube->hasBaseFilter()) {
+ $filter->add($cube->getBaseFilter());
+ }
+
+ foreach ($cube->getSlices() as $dimension => $slice) {
+ $filter->add(Filter::equal($dimension, $slice));
+ }
+
+ $url = Url::fromPath('icingadb/' . $type . 's');
+ $url->setFilter($filter);
+
+ if ($type === 'host') {
+ $this->addActionLink(
+ $url,
+ t('Show hosts status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'server'
+ );
+ } else {
+ $this->addActionLink(
+ $url,
+ t('Show services status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'cog'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/MonitoringActions.php b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
new file mode 100644
index 0000000..ae65c67
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
@@ -0,0 +1,53 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Module\Cube\Ido\IdoServiceStatusCube;
+use Icinga\Web\View;
+
+/**
+ * MonitoringActionLinks
+ *
+ * An action link hook implementation linking to matching hosts/services in the
+ * monitoring module
+ */
+class MonitoringActions extends ActionsHook
+{
+ public function prepareActionLinks(Cube $cube, View $view)
+ {
+ if ($cube instanceof IdoHostStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_host_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/hosts';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show hosts status'),
+ $view->translate('This shows all matching hosts and their current state in the monitoring module'),
+ 'host'
+ );
+ } elseif ($cube instanceof IdoServiceStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_service_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/services';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show services status'),
+ $view->translate('This shows all matching services and their current state in the monitoring module'),
+ 'host'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..a32310a
--- /dev/null
+++ b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,11 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+}
diff --git a/library/Cube/Web/ActionLink.php b/library/Cube/Web/ActionLink.php
new file mode 100644
index 0000000..c9ad87b
--- /dev/null
+++ b/library/Cube/Web/ActionLink.php
@@ -0,0 +1,103 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionLinksHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLink
+{
+ /** @var Url */
+ protected $url;
+
+ /** @var string */
+ protected $title;
+
+ /** @var string */
+ protected $description;
+
+ /** @var string */
+ protected $icon;
+
+ /**
+ * ActionLink constructor.
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ */
+ public function __construct(Url $url, $title, $description, $icon)
+ {
+ $this->url = $url;
+ $this->title = $title;
+ $this->description = $description;
+ $this->icon = $icon;
+ }
+
+ /**
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Render our icon
+ *
+ * @param View $view
+ * @return string
+ */
+ protected function renderIcon(View $view)
+ {
+ return $view->icon($this->getIcon());
+ }
+
+ /**
+ * @param View $view
+ * @return string
+ */
+ public function render(View $view)
+ {
+ return sprintf(
+ '<a href="%s">%s<span class="title">%s</span><p>%s</p></a>',
+ $this->getUrl(),
+ $this->renderIcon($view),
+ $view->escape($this->getTitle()),
+ $view->escape($this->getDescription())
+ );
+ }
+}
diff --git a/library/Cube/Web/ActionLinks.php b/library/Cube/Web/ActionLinks.php
new file mode 100644
index 0000000..4b84fac
--- /dev/null
+++ b/library/Cube/Web/ActionLinks.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionsHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLinks
+{
+ /** @var ActionLink[] */
+ protected $links = array();
+
+ /**
+ * Get all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube, View $view)
+ {
+ $html = array();
+
+ /** @var ActionsHook $hook */
+ foreach (Hook::all('Cube/Actions') as $hook) {
+ try {
+ $hook->prepareActionLinks($cube, $view);
+ } catch (Exception $e) {
+ $html[] = self::renderErrorItem($e, $view);
+ }
+
+ foreach ($hook->getActionLinks()->getLinks() as $link) {
+ $html[] = '<li>' . $link->render($view) . '</li>';
+ }
+ }
+
+ if (empty($html)) {
+ $html[] = self::renderErrorItem(
+ $view->translate('No action links have been provided for this cube'),
+ $view
+ );
+ }
+
+ return implode("\n", $html) . "\n";
+ }
+
+ /**
+ * @param Exception|string $error
+ * @param View $view
+ * @return string
+ */
+ private static function renderErrorItem($error, View $view)
+ {
+ if ($error instanceof Exception) {
+ $error = $error->getMessage();
+ }
+ return '<li class="error">' . $view->escape($error) . '</li>';
+ }
+
+ /**
+ * Add an ActionLink to this set of actions
+ *
+ * @param ActionLink $link
+ * @return $this
+ */
+ public function add(ActionLink $link)
+ {
+ $this->links[] = $link;
+ return $this;
+ }
+
+ /**
+ * @return ActionLink[]
+ */
+ public function getLinks()
+ {
+ return $this->links;
+ }
+
+ /**
+ * @param View $view
+ *
+ * @return string
+ */
+ public function render(View $view)
+ {
+ $links = $this->getLinks();
+ if (empty($links)) {
+ return '';
+ }
+
+ $html = '<ul class="action-links">';
+ foreach ($links as $link) {
+ $html .= $link->render($view);
+ }
+ $html .= '</ul>';
+
+ return $html;
+ }
+}
diff --git a/library/Cube/Web/Controller.php b/library/Cube/Web/Controller.php
new file mode 100644
index 0000000..028a744
--- /dev/null
+++ b/library/Cube/Web/Controller.php
@@ -0,0 +1,297 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Module\Cube\Forms\DimensionsForm;
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\CustomVariableDimension;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Web\Control\ProblemToggle;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Html\HtmlString;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Compat\SearchControls;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Tabs;
+
+abstract class Controller extends CompatController
+{
+ use SearchControls;
+ use Database;
+ use Auth;
+
+ /** @var string[] Preserved params for searchbar and search editor controls */
+ protected $preserveParams = [
+ 'dimensions',
+ 'showSettings',
+ 'wantNull',
+ 'problems',
+ 'sort'
+ ];
+
+ /** @var Filter\Rule Filter from query string parameters */
+ private $filter;
+
+ /**
+ * Return this controllers' cube
+ *
+ * @return IcingaDbCube
+ */
+ abstract protected function getCube(): IcingaDbCube;
+
+ /**
+ * Get the filter created from query string parameters
+ *
+ * @return Filter\Rule
+ */
+ public function getFilter(): Filter\Rule
+ {
+ if ($this->filter === null) {
+ $this->filter = QueryString::parse((string) $this->params);
+ }
+
+ return $this->filter;
+ }
+
+ public function detailsAction(): void
+ {
+ $cube = $this->prepareCube();
+ $this->getTabs()->add('details', [
+ 'label' => $this->translate('Cube details'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('details');
+
+ $cube->setBaseFilter($this->getFilter());
+
+ $this->setTitle($cube->getSlicesLabel());
+ $this->view->links = IcingaDbActionsHook::renderAll($cube);
+
+ $this->addContent(
+ HtmlString::create($this->view->render('/cube-details.phtml'))
+ );
+ }
+
+ protected function renderCube(): void
+ {
+ $cube = $this->prepareCube();
+ $this->setTitle(sprintf(
+ $this->translate('Cube: %s'),
+ $cube->getPathLabel()
+ ));
+
+ $this->params->shift('format');
+ $showSettings = $this->params->shift('showSettings');
+
+ $query = $cube->innerQuery();
+ $problemsOnly = (bool) $this->params->shift('problems', false);
+ $problemToggle = (new ProblemToggle($problemsOnly ?: null))
+ ->setIdProtector([$this->getRequest(), 'protectId'])
+ ->on(ProblemToggle::ON_SUCCESS, function (ProblemToggle $form) {
+ /** @var CheckboxElement $problems */
+ $problems = $form->getElement('problems');
+ if (! $problems->isChecked()) {
+ $this->redirectNow(Url::fromRequest()->remove('problems'));
+ } else {
+ $this->redirectNow(Url::fromRequest()->setParam('problems'));
+ }
+ })->handleRequest($this->getServerRequest());
+
+ $this->addControl($problemToggle);
+
+ $sortControl = SortControl::create([
+ IcingaDbCube::DIMENSION_VALUE_SORT_PARAM => t('Value'),
+ IcingaDbCube::DIMENSION_SEVERITY_SORT_PARAM . ' desc' => t('Severity'),
+ ]);
+
+ $this->params->shift($sortControl->getSortParam());
+ $cube->sortBy($sortControl->getSort());
+ $this->addControl($sortControl);
+
+ $searchBar = $this->createSearchBar(
+ $query,
+ $this->preserveParams
+ );
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ if ($problemsOnly) {
+ $filter = Filter::all($filter, Filter::equal('state.is_problem', true));
+ }
+
+ $cube->setBaseFilter($filter);
+ $cube->problemsOnly($problemsOnly);
+
+ $this->addControl($searchBar);
+
+ if (count($cube->listDimensions()) > 0) {
+ $this->view->cube = $cube;
+ } else {
+ $showSettings = true;
+ }
+
+ $this->view->url = Url::fromRequest()
+ ->onlyWith($this->preserveParams)
+ ->setFilter($searchBar->getFilter());
+
+ if ($showSettings) {
+ $form = (new DimensionsForm())
+ ->setUrl($this->view->url)
+ ->setCube($cube)
+ ->on(DimensionsForm::ON_SUCCESS, function ($form) {
+ $this->redirectNow($form->getRedirectUrl());
+ })
+ ->handleRequest($this->getServerRequest());
+
+ $this->view->form = $form;
+ } else {
+ $this->setAutorefreshInterval(15);
+ }
+
+ $this->addContent(
+ HtmlString::create($this->view->render('/cube-index.phtml'))
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+ }
+
+ private function prepareCube(): IcingaDbCube
+ {
+ $cube = $this->getCube();
+ $cube->chooseFacts(array_keys($cube->getAvailableFactColumns()));
+
+ $dimensions = DimensionParams::fromString(
+ $this->params->shift('dimensions', '')
+ )->getDimensions();
+
+ if ($this->hasLegacyDimensionParams($dimensions)) {
+ $this->transformLegacyDimensionParamsAndRedirect($dimensions);
+ }
+
+ $wantNull = $this->params->shift('wantNull');
+ foreach ($dimensions as $dimension) {
+ $cube->addDimensionByName($dimension);
+ if ($wantNull) {
+ $cube->getDimension($dimension)->wantNull();
+ }
+
+ $sliceParamWithPrefix = rawurlencode($cube::SLICE_PREFIX . $dimension);
+
+ if ($this->params->has($sliceParamWithPrefix)) {
+ $this->preserveParams[] = $sliceParamWithPrefix;
+ $cube->slice($dimension, $this->params->shift($sliceParamWithPrefix));
+ }
+ }
+
+ return $cube;
+ }
+
+ /**
+ * Get whether the given dimension param is legacy dimension param
+ *
+ * @param string $dimensionParam
+ *
+ * @return bool
+ */
+ private function isLegacyDimensionParam(string $dimensionParam): bool
+ {
+ return ! Str::startsWith($dimensionParam, CustomVariableDimension::HOST_PREFIX)
+ && ! Str::startsWith($dimensionParam, CustomVariableDimension::SERVICE_PREFIX);
+ }
+
+ /**
+ * Get whether the dimensions contain legacy dimension
+ *
+ * @param array $dimensions
+ *
+ * @return bool
+ */
+ private function hasLegacyDimensionParams(array $dimensions): bool
+ {
+ foreach ($dimensions as $dimension) {
+ if ($this->isLegacyDimensionParam($dimension)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Transform legacy dimension and slice params and redirect
+ *
+ * This adds the new prefix to params and then redirects so that the new URL contains the prefixed params
+ * Slices are prefixed to differ filter and slice params
+ *
+ * @param array $legacyDimensions
+ */
+ private function transformLegacyDimensionParamsAndRedirect(array $legacyDimensions): void
+ {
+ $dimensions = [];
+ $slices = [];
+
+ $dimensionPrefix = CustomVariableDimension::HOST_PREFIX;
+ if ($this->getRequest()->getControllerName() === 'services') {
+ $dimensionPrefix = CustomVariableDimension::SERVICE_PREFIX;
+ }
+
+ foreach ($legacyDimensions as $param) {
+ $newParam = $param;
+ if ($this->isLegacyDimensionParam($param)) {
+ $newParam = $dimensionPrefix . $param;
+ }
+
+ $slice = $this->params->shift($param);
+ if ($slice) {
+ $slices[IcingaDbCube::SLICE_PREFIX . $newParam] = $slice;
+ }
+
+ $dimensions[] = $newParam;
+ }
+
+ $this->redirectNow(
+ Url::fromRequest()
+ ->setParam('dimensions', DimensionParams::fromArray($dimensions)->getParams())
+ ->addParams($slices)
+ ->without($legacyDimensions)
+ );
+ }
+
+ public function createTabs(): Tabs
+ {
+ $params = Url::fromRequest()
+ ->onlyWith($this->preserveParams)
+ ->getParams()
+ ->toString();
+
+ return $this->getTabs()
+ ->add('cube/hosts', [
+ 'label' => $this->translate('Hosts'),
+ 'url' => 'cube/hosts' . ($params === '' ? '' : '?' . $params)
+ ])
+ ->add('cube/services', [
+ 'label' => $this->translate('Services'),
+ 'url' => 'cube/services' . ($params === '' ? '' : '?' . $params)
+ ]);
+ }
+}
diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php
new file mode 100644
index 0000000..a9feec9
--- /dev/null
+++ b/library/Cube/Web/IdoController.php
@@ -0,0 +1,198 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Module\Cube\Forms\DimensionsForm;
+use Icinga\Module\Cube\IcingaDb\CustomVariableDimension;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\Ido\IdoCube;
+use ipl\Stdlib\Str;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Url;
+use ipl\Web\Widget\Tabs;
+
+abstract class IdoController extends CompatController
+{
+ /**
+ * Return this controllers' cube
+ *
+ * @return IdoCube
+ */
+ abstract protected function getCube(): IdoCube;
+
+ public function detailsAction(): void
+ {
+ $cube = $this->prepareCube();
+
+ $this->getTabs()->add('details', [
+ 'label' => $this->translate('Cube details'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('details');
+
+ $this->view->title = $cube->getSlicesLabel();
+
+ $this->view->links = ActionLinks::renderAll($cube, $this->view);
+
+ $this->render('cube-details', null, true);
+ }
+
+ protected function renderCube(): void
+ {
+ $this->params->shift('format');
+ $showSettings = $this->params->shift('showSettings');
+
+ $cube = $this->prepareCube();
+
+ $this->view->title = sprintf(
+ $this->translate('Cube: %s'),
+ $cube->getPathLabel()
+ );
+
+ if (count($cube->listDimensions()) > 0) {
+ $this->view->cube = $cube;
+ } else {
+ $showSettings = true;
+ }
+
+ $this->view->url = Url::fromRequest();
+ if ($showSettings) {
+ $form = (new DimensionsForm())
+ ->setUrl($this->view->url)
+ ->setCube($cube)
+ ->setUrl(Url::fromRequest())
+ ->on(DimensionsForm::ON_SUCCESS, function ($form) {
+ $this->redirectNow($form->getRedirectUrl());
+ })
+ ->handleRequest($this->getServerRequest());
+
+ $this->view->form = $form;
+ } else {
+ $this->setAutorefreshInterval(15);
+ }
+
+ $this->render('cube-index', null, true);
+ }
+
+ private function prepareCube(): IdoCube
+ {
+ $cube = $this->getCube();
+ $cube->chooseFacts(array_keys($cube->getAvailableFactColumns()));
+
+ $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions();
+
+ $resolved = $this->params->shift('resolved', false);
+
+ if (
+ ! $resolved
+ && Module::exists('icingadb')
+ && $this->hasIcingadbDimensionParams($vars)
+ ) {
+ $this->transformIcingadbDimensionParamsAndRedirect($vars);
+ } elseif ($resolved) {
+ $this->redirectNow(Url::fromRequest()->without('resolved'));
+ }
+
+ $wantNull = $this->params->shift('wantNull');
+
+ foreach ($vars as $var) {
+ $cube->addDimensionByName($var);
+ if ($wantNull) {
+ $cube->getDimension($var)->wantNull();
+ }
+ }
+
+ foreach ($this->params->toArray() as $param) {
+ $cube->slice(rawurldecode($param[0]), rawurldecode($param[1]));
+ }
+
+ return $cube;
+ }
+
+ /**
+ * Get whether the dimensions contain icingadb dimension
+ *
+ * @param array $dimensions
+ *
+ * @return bool
+ */
+ private function hasIcingadbDimensionParams(array $dimensions): bool
+ {
+ foreach ($dimensions as $dimension) {
+ if (
+ Str::startsWith($dimension, CustomVariableDimension::HOST_PREFIX)
+ || Str::startsWith($dimension, CustomVariableDimension::SERVICE_PREFIX)
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Transform icingadb dimension and slice params and redirect
+ *
+ * This remove the new icingadb prefix from params and remove sort, problems-only, filter params
+ *
+ * @param array $icingadbDimensions
+ */
+ private function transformIcingadbDimensionParamsAndRedirect(array $icingadbDimensions): void
+ {
+ $dimensions = [];
+ $slices = [];
+ $toRemoveSlices = [];
+
+ $prefix = CustomVariableDimension::HOST_PREFIX;
+ if ($this->getRequest()->getControllerName() === 'ido-services') {
+ $prefix = CustomVariableDimension::SERVICE_PREFIX;
+ }
+
+ foreach ($icingadbDimensions as $param) {
+ $newParam = $param;
+ if (strpos($param, $prefix) !== false) {
+ $newParam = substr($param, strlen($prefix));
+ }
+
+ $slice = $this->params->shift(IcingaDbCube::SLICE_PREFIX . $param);
+ if ($slice) {
+ $slices[$newParam] = $slice;
+ $toRemoveSlices[] = IcingaDbCube::SLICE_PREFIX . $param;
+ }
+
+ $dimensions[] = $newParam;
+ }
+
+ $icingadbParams = array_merge(
+ $icingadbDimensions,
+ $toRemoveSlices,
+ array_keys($this->params->toArray(false))
+ );
+
+ $this->redirectNow(
+ Url::fromRequest()
+ ->setParam('dimensions', DimensionParams::fromArray($dimensions)->getParams())
+ ->addParams($slices)
+ ->addParams(['resolved' => true])
+ ->without($icingadbParams)
+ );
+ }
+
+ public function createTabs(): Tabs
+ {
+ $params = Url::fromRequest()->getParams()->toString();
+
+ return $this->getTabs()
+ ->add('cube/hosts', [
+ 'label' => $this->translate('Hosts'),
+ 'url' => 'cube/hosts' . ($params === '' ? '' : '?' . $params)
+ ])
+ ->add('cube/services', [
+ 'label' => $this->translate('Services'),
+ 'url' => 'cube/services' . ($params === '' ? '' : '?' . $params)
+ ]);
+ }
+}