summaryrefslogtreecommitdiffstats
path: root/library/Cube/IcingaDb
diff options
context:
space:
mode:
Diffstat (limited to 'library/Cube/IcingaDb')
-rw-r--r--library/Cube/IcingaDb/CustomVariableDimension.php166
-rw-r--r--library/Cube/IcingaDb/IcingaDbCube.php338
-rw-r--r--library/Cube/IcingaDb/IcingaDbHostStatusCube.php80
-rw-r--r--library/Cube/IcingaDb/IcingaDbServiceStatusCube.php94
4 files changed, 678 insertions, 0 deletions
diff --git a/library/Cube/IcingaDb/CustomVariableDimension.php b/library/Cube/IcingaDb/CustomVariableDimension.php
new file mode 100644
index 0000000..34a395c
--- /dev/null
+++ b/library/Cube/IcingaDb/CustomVariableDimension.php
@@ -0,0 +1,166 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter;
+
+class CustomVariableDimension implements Dimension
+{
+ use Auth;
+
+ /** @var string Prefix for host custom variable */
+ public const HOST_PREFIX = 'host.vars.';
+
+ /** @var string Prefix for service custom variable */
+ public const SERVICE_PREFIX = 'service.vars.';
+
+ /** @var ?string variable source name */
+ protected $sourceName;
+
+ /** @var ?string Variable name without prefix */
+ protected $varName;
+
+ /** @var string Variable name with prefix */
+ protected $name;
+
+ protected $label;
+
+ protected $wantNull = false;
+
+ public function __construct($name)
+ {
+ if (preg_match('/^(host|service)\.vars\.(.*)/', $name, $matches)) {
+ $this->sourceName = $matches[1];
+ $this->varName = $matches[2];
+ }
+
+ $this->name = $name;
+ }
+
+ /**
+ * Get the variable name without prefix
+ *
+ * @return string
+ */
+ public function getVarName(): string
+ {
+ return $this->varName ?? $this->getName();
+ }
+
+ /**
+ * Get the variable source name
+ *
+ * @return ?string
+ */
+ public function getSourceName(): ?string
+ {
+ return $this->sourceName;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getLabel()
+ {
+ return $this->label ?: $this->getName();
+ }
+
+ public function setLabel($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ public function addLabel($label)
+ {
+ if ($this->label === null) {
+ $this->setLabel($label);
+ } else {
+ $this->label .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaDbCube $cube
+ * @return Expression|string
+ */
+ public function getColumnExpression(Cube $cube)
+ {
+ $expression = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias(), 'flatvalue']);
+
+ if ($this->wantNull) {
+ return new Expression("COALESCE($expression, '-')");
+ }
+
+ return $expression;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ /** @var IcingaDbCube $cube */
+ $innerQuery = $cube->innerQuery();
+ $sourceTable = $this->getSourceName() ?? $innerQuery->getModel()->getTableName();
+
+ $subQuery = $innerQuery->createSubQuery(new CustomvarFlat(), $sourceTable . '.vars');
+ $subQuery->getSelectBase()->resetWhere(); // The link to the outer query is the ON condition
+ $subQuery->columns(['flatvalue', 'object_id' => $sourceTable . '.id']);
+ $subQuery->filter(Filter::like('flatname', $this->getVarName()));
+
+ // Values might not be unique (wildcard dimensions)
+ $subQueryModelAlias = $subQuery->getResolver()->getAlias($subQuery->getModel());
+ $subQuery->getSelectBase()->groupBy([
+ $subQueryModelAlias . '.flatname', // Required by postgres, if there are any custom variable protections
+ $subQueryModelAlias . '.flatvalue',
+ 'object_id'
+ ]);
+
+ $this->applyRestrictions($subQuery);
+
+ $subQueryAlias = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias()]);
+ $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue');
+
+ $sourceIdPath = '.id';
+ if ($innerQuery->getModel() instanceof Service && $sourceTable === 'host') {
+ $sourceIdPath = '.host_id';
+ }
+
+ $innerQuery->getSelectBase()->join(
+ [$subQueryAlias => $subQuery->assembleSelect()],
+ [
+ $subQueryAlias . '.object_id = '
+ . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . $sourceIdPath
+ ]
+ );
+ }
+
+ protected function createCustomVarAlias(): string
+ {
+ return implode('_', ['c', $this->getSourceName(), $this->getVarName()]);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php
new file mode 100644
index 0000000..44c7619
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbCube.php
@@ -0,0 +1,338 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Orm\Common\SortUtil;
+use ipl\Orm\Query;
+use ipl\Sql\Adapter\Pgsql;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+use ipl\Stdlib\BaseFilter;
+
+abstract class IcingaDbCube extends Cube
+{
+ use Auth;
+ use BaseFilter;
+ use Database;
+
+ public const SLICE_PREFIX = 'slice.';
+ public const IS_USING_ICINGADB = true;
+
+ /** @var bool Whether to show problems only */
+ protected $problemsOnly = false;
+
+ /** @var string Sort param used to sort dimensions by value */
+ public const DIMENSION_VALUE_SORT_PARAM = 'value';
+
+ /** @var string Sort param used to sort dimensions by severity */
+ public const DIMENSION_SEVERITY_SORT_PARAM = 'severity';
+
+ /** @var Query The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ protected $objectsFilter;
+
+ /** @var array The sort order of dimensions, column as key and direction as value */
+ protected $sortBy;
+
+ abstract public function getObjectsFilter();
+ /**
+ * An IcingaDbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return Query
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return Query
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = $this->prepareInnerQuery();
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * Get our rollup query
+ *
+ * @return Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ $this->addDimension($this->createDimension($name));
+
+ return $this;
+ }
+
+ /**
+ * Set whether to show problems only
+ *
+ * @param bool $problemOnly
+ *
+ * @return $this
+ */
+ public function problemsOnly(bool $problemOnly = true): self
+ {
+ $this->problemsOnly = $problemOnly;
+
+ return $this;
+ }
+
+
+ /**
+ * Get whether to show problems only
+ *
+ * @return bool
+ */
+ public function isProblemsOnly(): bool
+ {
+ return $this->problemsOnly;
+ }
+
+ /**
+ * Fetch the host variable dimensions
+ *
+ * @return array
+ */
+ public function fetchHostVariableDimensions(): array
+ {
+ $query = Host::on($this->getDb())
+ ->with('customvar_flat')
+ ->columns('customvar_flat.flatname')
+ ->orderBy('customvar_flat.flatname');
+
+ $this->applyRestrictions($query);
+
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->customvar_flat->flatname);
+ $name = strtolower($name);
+ $dimensions[CustomVariableDimension::HOST_PREFIX . $name] = 'Host ' . $name;
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * Fetch the service variable dimensions
+ *
+ * @return array
+ */
+ public function fetchServiceVariableDimensions(): array
+ {
+ $query = Service::on($this->getDb())
+ ->with('customvar_flat')
+ ->columns('customvar_flat.flatname')
+ ->orderBy('customvar_flat.flatname');
+
+ $this->applyRestrictions($query);
+
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->customvar_flat->flatname);
+ $name = strtolower($name);
+ $dimensions[CustomVariableDimension::SERVICE_PREFIX . $name] = 'Service ' . $name;
+ }
+
+ return $dimensions;
+ }
+
+ /**
+ * Set sort by columns
+ *
+ * @param ?string $sortBy
+ *
+ * @return $this
+ */
+ public function sortBy(?string $sortBy): self
+ {
+ if (empty($sortBy)) {
+ return $this;
+ }
+
+ $this->sortBy = SortUtil::createOrderBy($sortBy)[0];
+
+ return $this;
+ }
+
+ /**
+ * Get sort by columns
+ *
+ * @return ?array Column as key and direction as value
+ */
+ public function getSortBy(): ?array
+ {
+ return $this->sortBy;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ protected function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery();
+ $select = $query->getSelectBase();
+
+ $columns = [];
+ foreach ($this->dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+ $dimension->addToCube($this);
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+
+ if ($this->hasSlice($name)) {
+ $select->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $select->columns($columns);
+
+ $this->applyRestrictions($query);
+ if ($this->hasBaseFilter()) {
+ $query->filter($this->getBaseFilter());
+ }
+ }
+
+ protected function prepareRollupQuery()
+ {
+ $dimensions = $this->listDimensions();
+ $this->finalizeInnerQuery();
+
+ $columns = [];
+ $groupBy = [];
+ foreach ($dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+
+ $columns[$quotedDimension] = 'f.' . $quotedDimension;
+ $groupBy[] = $quotedDimension;
+ }
+
+ $availableFacts = $this->getAvailableFactColumns();
+
+ foreach ($this->chosenFacts as $alias) {
+ $columns[$alias] = new Expression('SUM(f.' . $availableFacts[$alias] . ')');
+ }
+
+ if (! empty($groupBy)) {
+ if ($this->getDb()->getAdapter() instanceof Pgsql) {
+ $groupBy = 'ROLLUP(' . implode(', ', $groupBy) . ')';
+ } else {
+ $groupBy[count($groupBy) - 1] .= ' WITH ROLLUP';
+ }
+ }
+
+ $rollupQuery = new Select();
+ $rollupQuery->from(['f' => $this->innerQuery()->assembleSelect()])
+ ->columns($columns)
+ ->groupBy($groupBy);
+
+ return $rollupQuery;
+ }
+
+ protected function prepareFullQuery()
+ {
+ $rollupQuery = $this->rollupQuery();
+ $columns = [];
+ $orderBy = [];
+ $sortBy = $this->getSortBy();
+ foreach ($this->listColumns() as $column) {
+ $quotedColumn = $this->getDb()->quoteIdentifier([$column]);
+ $columns[$quotedColumn] = 'rollup.' . $quotedColumn;
+
+ if ($this->hasDimension($column)) {
+ $orderBy["($quotedColumn IS NOT NULL)"] = null;
+
+ $sortDir = 'ASC';
+ if ($sortBy && self::DIMENSION_VALUE_SORT_PARAM === $sortBy[0]) {
+ $sortDir = $sortBy[1] ?? 'ASC';
+ }
+
+ $orderBy[$quotedColumn] = $sortDir;
+ }
+ }
+
+ return (new Select())
+ ->from(['rollup' => $rollupQuery])
+ ->columns($columns)
+ ->orderBy($orderBy);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->getDb()->fetchAll($query);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
new file mode 100644
index 0000000..14e083f
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
@@ -0,0 +1,80 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+
+class IcingaDbHostStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'hosts_cnt' => 'hosts_total',
+ 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled',
+ 'hosts_unhandled_down' => 'hosts_down_unhandled',
+ ];
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function listAvailableDimensions()
+ {
+ return $this->fetchHostVariableDimensions();
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = HoststateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Host())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ $this->innerQuery = $query;
+ return $this->innerQuery;
+ }
+
+ /**
+ * Return Filter for Hosts cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $hosts = $this->innerQuery()->columns(['host' => 'host.name']);
+ $hosts->getSelectBase()->resetGroupBy();
+
+ $filter = Filter::any();
+
+ foreach ($hosts as $object) {
+ $filter->add(Filter::equal('host.name', $object->host));
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
new file mode 100644
index 0000000..ac59de2
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
@@ -0,0 +1,94 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Str;
+
+class IcingaDbServiceStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'services_total',
+ 'services_critical' => 'services_critical_handled + f.services_critical_unhandled',
+ 'services_unhandled_critical' => 'services_critical_unhandled',
+ 'services_warning' => 'services_warning_handled + f.services_warning_unhandled',
+ 'services_unhandled_warning' => 'services_warning_unhandled',
+ 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled',
+ 'services_unhandled_unknown' => 'services_unknown_unhandled',
+ ];
+ }
+
+ public function listAvailableDimensions()
+ {
+ return array_merge(
+ $this->fetchServiceVariableDimensions(),
+ $this->fetchHostVariableDimensions()
+ );
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = ServicestateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Service())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ return $query;
+ }
+
+ /**
+ * Return Filter for Services cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $services = $this->innerQuery()->columns([
+ 'host_name' => 'host.name',
+ 'service_name' => 'service.name'
+ ]);
+
+ $services->getSelectBase()->resetGroupBy();
+ $filter = Filter::any();
+
+ foreach ($services as $service) {
+ $filter->add(
+ Filter::all(
+ Filter::equal('service.name', $service->service_name),
+ Filter::equal('host.name', $service->host_name)
+ )
+ );
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}