summaryrefslogtreecommitdiffstats
path: root/library/Cube/Ido
diff options
context:
space:
mode:
Diffstat (limited to 'library/Cube/Ido')
-rw-r--r--library/Cube/Ido/CustomVarDimension.php158
-rw-r--r--library/Cube/Ido/DataView/Hoststatus.php17
-rw-r--r--library/Cube/Ido/DbCube.php298
-rw-r--r--library/Cube/Ido/IdoCube.php198
-rw-r--r--library/Cube/Ido/IdoHostStatusCube.php112
-rw-r--r--library/Cube/Ido/IdoServiceStatusCube.php113
-rw-r--r--library/Cube/Ido/Query/HoststatusQuery.php47
-rw-r--r--library/Cube/Ido/ZfSelectWrapper.php77
8 files changed, 1020 insertions, 0 deletions
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);
+ }
+}