diff options
Diffstat (limited to 'library/Cube')
27 files changed, 3329 insertions, 0 deletions
diff --git a/library/Cube/Cube.php b/library/Cube/Cube.php new file mode 100644 index 0000000..b307869 --- /dev/null +++ b/library/Cube/Cube.php @@ -0,0 +1,323 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Exception\IcingaException; +use Icinga\Web\View; + +abstract class Cube +{ + /** @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(); + + 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 $label) { + $name = strtolower($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 (array_key_exists($name, $this->dimensions)) { + 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..3f8c80d --- /dev/null +++ b/library/Cube/CubeRenderer.php @@ -0,0 +1,406 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Web\View; + +/** + * 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(); + + /** + * 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(); + foreach ($this->cube->fetchAll() as $row) { + $htm .= $this->renderRow($row); + } + + return $htm . $this->closeDimensions() . $this->endContainer(); + } + + 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) + { + $cube = $this->cube; + + $dimensions = array_merge(array_keys($cube->listDimensions()), $cube->listSlices()); + $params = [ + 'dimensions' => DimensionParams::update($dimensions)->getParams() + ]; + + foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) { + $params[$dimensionName] = $row->$dimensionName; + } + + foreach ($this->cube->getSlices() as $key => $val) { + $params[$key] = $val; + } + + return $this->view->url( + $this->getDetailsBaseUrl(), + $params + ); + } + + protected function getSliceUrl($name, $row) + { + return $this->view->url() + ->setParam($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..89322cb --- /dev/null +++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php @@ -0,0 +1,105 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\CubeRenderer; + +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; + if ($sums->hosts_down > 0) { + $classes[] = 'critical'; + if ((int) $sums->hosts_unhandled_down === 0) { + $classes[] = 'handled'; + } + } elseif ($sums->hosts_unreachable > 0) { + $classes[] = 'unreachable'; + if ((int) $sums->hosts_unhandled_unreachable === 0) { + $classes[] = 'handled'; + } + } else { + $classes[] = 'ok'; + } + + return $classes; + } + + public function renderFacts($facts) + { + $indent = str_repeat(' ', 3); + $parts = array(); + + if ($facts->hosts_unhandled_down > 0) { + $parts['critical'] = $facts->hosts_unhandled_down; + } + + if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) { + $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down; + } + + if ($facts->hosts_unhandled_unreachable > 0) { + $parts['unreachable'] = $facts->hosts_unhandled_unreachable; + } + + if ($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 && $facts->hosts_cnt > $facts->hosts_unreachable) { + $parts['ok'] = $facts->hosts_cnt - $facts->hosts_down - $facts->hosts_unreachable; + } + + $main = ''; + $sub = ''; + foreach ($parts as $class => $count) { + 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'; + } +} diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php new file mode 100644 index 0000000..33caa84 --- /dev/null +++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php @@ -0,0 +1,126 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\CubeRenderer; + +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_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) { + $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical; + } + + if ($facts->services_unhandled_warning > 0) { + $parts['warning'] = $facts->services_unhandled_warning; + } + + 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_unhandled_unknown > 0) { + $parts['unknown'] = $facts->services_unhandled_unknown; + } + + 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_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 ($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; + if ($sums->services_critical > 0) { + $classes[] = 'critical'; + if ((int) $sums->services_unhandled_critical === 0) { + $classes[] = 'handled'; + } + } elseif ($sums->services_warning > 0) { + $classes[] = 'warning'; + if ((int) $sums->services_unhandled_warning === 0) { + $classes[] = 'handled'; + } + } elseif ($sums->services_unknown > 0) { + $classes[] = 'unknown'; + if ((int) $sums->services_unhandled_unknown === 0) { + $classes[] = 'handled'; + } + } else { + $classes[] = 'ok'; + } + + return $classes; + } + + 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'; + } +} diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php new file mode 100644 index 0000000..7519da4 --- /dev/null +++ b/library/Cube/Dimension.php @@ -0,0 +1,52 @@ +<?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(); + + /** + * The label of this dimension + * + * @return string + */ + public function getLabel(); + + /** + * 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..02b7ea1 --- /dev/null +++ b/library/Cube/DimensionParams.php @@ -0,0 +1,87 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Web\Url; + +class DimensionParams +{ + /** + * @var array Raw dimensions + */ + protected $dimensions = []; + + /** + * @var string encoded dimensions separated by coma + */ + protected $params; + + // For the form: DimensionsParam::fromUrl($url) + 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; + } + + // For the controller: DimensionsParam::fromArray($this->params->shift('dimensions')) + public static function fromString($dimensions) + { + return static::fromArray(explode(',', $dimensions)); + } + + + /** + * @param $dimension + * + * @return $this + */ + public function add($dimension) + { + if (! empty($dimension)) { + $this->dimensions[] = $dimension; + } + + return $this; + } + + /** + * Overwrite dimensions + * + * @param $dimensions + * + * @return $this + */ + 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..2266395 --- /dev/null +++ b/library/Cube/IcingaDb/CustomVariableDimension.php @@ -0,0 +1,119 @@ +<?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\Model\CustomvarFlat; +use ipl\Sql\Expression; +use ipl\Stdlib\Filter; + +class CustomVariableDimension implements Dimension +{ + protected $name; + + protected $label; + + protected $wantNull = false; + + public function __construct($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getLabel() + { + return $this->label ?: $this->getName(); + } + + /** + * Set the label + * + * @param string $label + * @return $this + */ + public function setLabel($label) + { + $this->label = $label; + + return $this; + } + + /** + * Add a label + * + * @param string $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(['c_' . $this->getName(), 'flatvalue']); + + if ($this->wantNull) { + return new Expression("COALESCE($expression, '-')"); + } + + return $expression; + } + + public function addToCube(Cube $cube) + { + /** @var IcingaDbCube $cube */ + $name = $this->getName(); + $innerQuery = $cube->innerQuery(); + $sourceTable = $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', $name)); + + // Values might not be unique (wildcard dimensions) + $subQuery->getSelectBase()->groupBy([ + $subQuery->getResolver()->getAlias($subQuery->getModel()) . '.flatvalue', + 'object_id' + ]); + + $subQueryAlias = $cube->getDb()->quoteIdentifier(['c_' . $name]); + $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue'); + $innerQuery->getSelectBase()->join( + [$subQueryAlias => $subQuery->assembleSelect()], + [$subQueryAlias . '.object_id = ' . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . '.id'] + ); + } +} diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php new file mode 100644 index 0000000..3f38d9b --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbCube.php @@ -0,0 +1,196 @@ +<?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 ipl\Orm\Query; +use ipl\Sql\Adapter\Pgsql; +use ipl\Sql\Expression; +use ipl\Sql\Select; + +abstract class IcingaDbCube extends Cube +{ + use Auth; + use Database; + + /** @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; + + 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; + } + + /** + * 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()->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)) { + $query->where( + $dimension->getColumnExpression($this) . ' = ?', + $this->slices[$name] + ); + } else { + $columns[$quotedDimension] = $dimension->getColumnExpression($this); + } + } + + $query->columns($columns); + } + + 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 = []; + foreach ($this->listColumns() as $column) { + $quotedColumn = $this->getDb()->quoteIdentifier([$column]); + $columns[$quotedColumn] = 'rollup.' . $this->getDb()->quoteIdentifier([$column]); + } + + $fullQuery = new Select(); + $fullQuery->from(['rollup' => $rollupQuery])->columns($columns); + + foreach ($columns as $quotedColumn => $_) { + $fullQuery->orderBy("($quotedColumn IS NOT NULL)"); + $fullQuery->orderBy($quotedColumn); + } + + return $fullQuery; + } + + /** + * 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..028d4d7 --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php @@ -0,0 +1,100 @@ +<?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\CustomvarFlat; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\HoststateSummary; +use ipl\Stdlib\Filter; + +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', + 'hosts_unreachable' => 'hosts_unreachable', + 'hosts_unhandled_unreachable' => 'hosts_unreachable_unhandled' + ]; + } + + public function createDimension($name) + { + $this->registerAvailableDimensions(); + + if (isset($this->availableDimensions[$name])) { + return clone $this->availableDimensions[$name]; + } + + return new CustomVariableDimension($name); + } + + public function listAvailableDimensions() + { + $db = $this->getDb(); + + $query = CustomvarFlat::on($db); + $this->applyRestrictions($query); + + $query + ->columns('flatname') + ->orderBy('flatname') + ->filter(Filter::like('host.id', '*')); + $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->flatname); + $dimensions[$name] = $name; + } + + return $dimensions; + } + + 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..9336cdf --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php @@ -0,0 +1,109 @@ +<?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\CustomvarFlat; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Model\ServicestateSummary; +use ipl\Stdlib\Filter; + +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() + { + $db = $this->getDb(); + + $query = CustomvarFlat::on($db); + $this->applyRestrictions($query); + + $query + ->columns('flatname') + ->orderBy('flatname') + ->filter(Filter::like('service.id', '*')); + $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->flatname); + $dimensions[$name] = $name; + } + + return $dimensions; + } + + 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..0c21282 --- /dev/null +++ b/library/Cube/Ido/CustomVarDimension.php @@ -0,0 +1,158 @@ +<?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(); + } + + /** + * Set the label + * + * @param string $label + * @return $this + */ + public function setLabel($label) + { + $this->varLabel = $label; + + return $this; + } + + /** + * Add a label + * + * @param string $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 $cube IdoCube */ + $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..ca76b21 --- /dev/null +++ b/library/Cube/Ido/IdoCube.php @@ -0,0 +1,198 @@ +<?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; + + /** + * 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..4881a9b --- /dev/null +++ b/library/Cube/Ido/IdoHostStatusCube.php @@ -0,0 +1,112 @@ +<?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); + } + + /** + * Add a specific named dimension + * + * Right now this are just custom vars, we might support group memberships + * or other properties in future + * + * @param string $name + * @return $this + */ + public function addDimensionByName($name) + { + if (count($this->filterProtectedCustomvars(array($name))) === 1) { + $this->addDimension($this->createDimension($name)); + } + + return $this; + } + + /** + * 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'); + + return $this->filterProtectedCustomvars($this->db()->fetchCol($select)); + } + + 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..10a172a --- /dev/null +++ b/library/Cube/Ido/IdoServiceStatusCube.php @@ -0,0 +1,113 @@ +<?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'); + + return $this->filterProtectedCustomvars($this->db()->fetchCol($select)); + } + + 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; + } + + /** + * Add a specific named dimension + * + * Right now this are just custom vars, we might support group memberships + * or other properties in future + * + * @param string $name + * + * @return $this + */ + public function addDimensionByName($name) + { + if (count($this->filterProtectedCustomvars([$name])) === 1) { + $this->addDimension($this->createDimension($name)); + } + + return $this; + } + + 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..c670ab4 --- /dev/null +++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php @@ -0,0 +1,41 @@ +<?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; + +class IcingaDbActions extends IcingaDbActionsHook +{ + public function createActionLinks(IcingaDbCube $cube) + { + $type = 'host'; + if ($cube instanceof IcingadbServiceStatusCube) { + $type = 'service'; + } + + $url = 'icingadb/' . $type . 's'; + + $paramsWithPrefix = []; + foreach ($cube->getSlices() as $dimension => $slice) { + $paramsWithPrefix[$type . '.vars.' . $dimension] = $slice; + } + + if ($type === 'host') { + $this->addActionLink( + $this->makeUrl($url, $paramsWithPrefix), + t('Show hosts status'), + t('This shows all matching hosts and their current state in Icinga DB Web'), + 'server' + ); + } else { + $this->addActionLink( + $this->makeUrl($url, $paramsWithPrefix), + 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..0c6cba4 --- /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[] = static::renderErrorItem($e, $view); + } + + foreach ($hook->getActionLinks()->getLinks() as $link) { + $html[] = '<li>' . $link->render($view) . '</li>'; + } + } + + if (empty($html)) { + $html[] = static::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..b7405f9 --- /dev/null +++ b/library/Cube/Web/Controller.php @@ -0,0 +1,115 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 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\Hook\IcingaDbActionsHook; +use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Web\Controller as WebController; +use Icinga\Web\View; + +abstract class Controller extends WebController +{ + /** @var View This helps IDEs to understand that this is not ZF view */ + public $view; + + /** @var \Icinga\Module\Cube\Cube */ + protected $cube; + + /** + * Return this controllers' cube + * + * @return \Icinga\Module\Cube\Cube + */ + abstract protected function getCube(); + + protected function moduleInit() + { + $this->cube = $this->getCube(); + } + + public function detailsAction() + { + $this->getTabs()->add('details', [ + 'label' => $this->translate('Cube details'), + 'url' => $this->getRequest()->getUrl() + ])->activate('details'); + + $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns())); + $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions(); + $wantNull = $this->params->shift('wantNull'); + + foreach ($vars as $var) { + $this->cube->addDimensionByName($var); + if ($wantNull) { + $this->cube->getDimension($var)->wantNull(); + } + } + + foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) { + $this->params->shift($p); + } + + foreach ($this->params->toArray() as $param) { + $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1])); + } + + $this->view->title = $this->cube->getSlicesLabel(); + if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) { + $this->view->links = IcingaDbActionsHook::renderAll($this->cube); + } else { + $this->view->links = ActionLinks::renderAll($this->cube, $this->view); + } + + $this->render('cube-details', null, true); + } + + protected function renderCube() + { + // Hint: order matters, we are shifting! + $showSettings = $this->params->shift('showSettings'); + + $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns())); + $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions(); + $wantNull = $this->params->shift('wantNull'); + + foreach ($vars as $var) { + $this->cube->addDimensionByName($var); + if ($wantNull) { + $this->cube->getDimension($var)->wantNull(); + } + } + + foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) { + $this->params->shift($p); + } + + foreach ($this->params->toArray() as $param) { + $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1])); + } + + $this->view->title = sprintf( + $this->translate('Cube: %s'), + $this->cube->getPathLabel() + ); + + if (count($this->cube->listDimensions()) > 0) { + $this->view->cube = $this->cube; + } else { + $showSettings = true; + } + + if ($showSettings) { + $this->view->form = (new DimensionsForm())->setCube($this->cube); + $this->view->form->handleRequest(); + } else { + $this->setAutorefreshInterval(15); + } + + $this->render('cube-index', null, true); + } +} diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php new file mode 100644 index 0000000..2a024ae --- /dev/null +++ b/library/Cube/Web/IdoController.php @@ -0,0 +1,24 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Web; + +use Icinga\Web\Widget\Tabextension\DashboardAction; + +abstract class IdoController extends Controller +{ + public function createTabs() + { + return $this->getTabs() + ->add('cube/hosts', [ + 'label' => $this->translate('Hosts'), + 'url' => 'cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString()) + ]) + ->add('cube/services', [ + 'label' => $this->translate('Services'), + 'url' => 'cube/services' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString()) + ]) + ->extend(new DashboardAction()); + } +} |