diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:16:36 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:16:36 +0000 |
commit | d61b7618d9c04ff90fdf8d3b584ad5976faedad9 (patch) | |
tree | 6de6eaca7793f0f1f756c9a5a0fa9e07957c8569 /library/Cube | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-cube-upstream.tar.xz icingaweb2-module-cube-upstream.zip |
Adding upstream version 1.3.2.upstream/1.3.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Cube')
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) + ]); + } +} |