summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/ProvidedHook
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icingadb/ProvidedHook/ApplicationState.php111
-rw-r--r--library/Icingadb/ProvidedHook/CreateHostSlaReport.php37
-rw-r--r--library/Icingadb/ProvidedHook/CreateHostsSlaReport.php39
-rw-r--r--library/Icingadb/ProvidedHook/CreateServiceSlaReport.php40
-rw-r--r--library/Icingadb/ProvidedHook/CreateServicesSlaReport.php38
-rw-r--r--library/Icingadb/ProvidedHook/IcingaHealth.php115
-rw-r--r--library/Icingadb/ProvidedHook/RedisHealth.php55
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php68
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php72
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/SlaReport.php297
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php19
-rw-r--r--library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php19
-rw-r--r--library/Icingadb/ProvidedHook/X509/Sni.php55
13 files changed, 965 insertions, 0 deletions
diff --git a/library/Icingadb/ProvidedHook/ApplicationState.php b/library/Icingadb/ProvidedHook/ApplicationState.php
new file mode 100644
index 0000000..8c7b008
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/ApplicationState.php
@@ -0,0 +1,111 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Exception;
+use Icinga\Application\Hook\ApplicationStateHook;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Common\IcingaRedis;
+use Icinga\Module\Icingadb\Model\Instance;
+use Icinga\Web\Session;
+use ipl\Stdlib\Filter;
+
+class ApplicationState extends ApplicationStateHook
+{
+ use Database;
+
+ public function collectMessages()
+ {
+ try {
+ $lastIcingaHeartbeat = IcingaRedis::getLastIcingaHeartbeat();
+ } catch (Exception $e) {
+ $downSince = Session::getSession()->getNamespace('icingadb')->get('redis.down-since');
+
+ if ($downSince === null) {
+ $downSince = time();
+ Session::getSession()->getNamespace('icingadb')->set('redis.down-since', $downSince);
+ }
+
+ $this->addError(
+ 'icingadb/redis-down',
+ $downSince,
+ sprintf(t("Can't connect to Icinga Redis: %s"), $e->getMessage())
+ );
+
+ return;
+ }
+
+ $instance = Instance::on($this->getDb())
+ ->with(['endpoint'])
+ ->filter(Filter::equal('responsible', true))
+ ->orderBy('heartbeat', 'desc')
+ ->first();
+
+ if ($instance === null) {
+ $noInstanceSince = Session::getSession()
+ ->getNamespace('icingadb')->get('icingadb.no-instance-since');
+
+ if ($noInstanceSince === null) {
+ $noInstanceSince = time();
+ Session::getSession()
+ ->getNamespace('icingadb')->set('icingadb.no-instance-since', $noInstanceSince);
+ }
+
+ $this->addError(
+ 'icingadb/no-instance',
+ $noInstanceSince,
+ t(
+ 'It seems that Icinga DB is not running.'
+ . ' Make sure Icinga DB is running and writing into the database.'
+ )
+ );
+
+ return;
+ } else {
+ Session::getSession()->getNamespace('icingadb')->delete('db.no-instance-since');
+ }
+
+ $outdatedDbHeartbeat = $instance->heartbeat->getTimestamp() < time() - 60;
+
+ if ($lastIcingaHeartbeat === null) {
+ $missingSince = Session::getSession()
+ ->getNamespace('icingadb')->get('redis.heartbeat-missing-since');
+
+ if ($missingSince === null) {
+ $missingSince = time();
+ Session::getSession()
+ ->getNamespace('icingadb')->set('redis.heartbeat-missing-since', $missingSince);
+ }
+
+ $lastIcingaHeartbeat = $missingSince;
+ } else {
+ Session::getSession()->getNamespace('icingadb')->delete('redis.heartbeat-missing-since');
+ }
+
+ switch (true) {
+ case $outdatedDbHeartbeat && $instance->heartbeat->getTimestamp() > $lastIcingaHeartbeat:
+ $this->addError(
+ 'icingadb/redis-outdated',
+ $lastIcingaHeartbeat,
+ t('Icinga Redis is outdated. Make sure Icinga 2 is running and connected to Redis.')
+ );
+
+ break;
+ case $outdatedDbHeartbeat:
+ $this->addError(
+ 'icingadb/icingadb-down',
+ $instance->heartbeat->getTimestamp(),
+ t(
+ 'It seems that Icinga DB is not running.'
+ . ' Make sure Icinga DB is running and writing into the database.'
+ )
+ );
+
+ break;
+ }
+
+ Session::getSession()->getNamespace('icingadb')->delete('redis.down-since');
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/CreateHostSlaReport.php b/library/Icingadb/ProvidedHook/CreateHostSlaReport.php
new file mode 100644
index 0000000..83ed911
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/CreateHostSlaReport.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Icingadb\Hook\HostActionsHook;
+use Icinga\Module\Icingadb\Model\Host;
+use ipl\I18n\Translation;
+use ipl\Stdlib\Filter;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+class CreateHostSlaReport extends HostActionsHook
+{
+ use Translation;
+
+ public function getActionsForObject(Host $host): array
+ {
+ if (! Auth::getInstance()->hasPermission('reporting/reports')) {
+ return [];
+ }
+
+ $filter = QueryString::render(Filter::equal('host.name', $host->name));
+
+ return [
+ new Link(
+ $this->translate('Create Host SLA Report'),
+ Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'host']),
+ [
+ 'data-icinga-modal' => true,
+ 'data-no-icinga-ajax' => true
+ ]
+ )
+ ];
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/CreateHostsSlaReport.php b/library/Icingadb/ProvidedHook/CreateHostsSlaReport.php
new file mode 100644
index 0000000..6da9fca
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/CreateHostsSlaReport.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Icingadb\Hook\HostsDetailExtensionHook;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\ValidHtml;
+use ipl\I18n\Translation;
+use ipl\Orm\Query;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+class CreateHostsSlaReport extends HostsDetailExtensionHook
+{
+ use Translation;
+
+ public function getHtmlForObjects(Query $hosts): ValidHtml
+ {
+ if (Auth::getInstance()->hasPermission('reporting/reports')) {
+ $filter = QueryString::render($this->getBaseFilter());
+
+ return (new HtmlDocument())
+ ->addHtml(Html::tag('h2', $this->translate('Reporting')))
+ ->addHtml(new Link(
+ $this->translate('Create Host SLA Report'),
+ Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'host']),
+ [
+ 'data-icinga-modal' => true,
+ 'data-no-icinga-ajax' => true
+ ]
+ ));
+ }
+
+ return new HtmlDocument();
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/CreateServiceSlaReport.php b/library/Icingadb/ProvidedHook/CreateServiceSlaReport.php
new file mode 100644
index 0000000..eeab603
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/CreateServiceSlaReport.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Icingadb\Hook\ServiceActionsHook;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\I18n\Translation;
+use ipl\Stdlib\Filter;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+class CreateServiceSlaReport extends ServiceActionsHook
+{
+ use Translation;
+
+ public function getActionsForObject(Service $service): array
+ {
+ if (! Auth::getInstance()->hasPermission('reporting/reports')) {
+ return [];
+ }
+
+ $filter = QueryString::render(Filter::all(
+ Filter::equal('service.name', $service->name),
+ Filter::equal('host.name', $service->host->name)
+ ));
+
+ return [
+ new Link(
+ $this->translate('Create Service SLA Report'),
+ Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'service']),
+ [
+ 'data-icinga-modal' => true,
+ 'data-no-icinga-ajax' => true
+ ]
+ )
+ ];
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/CreateServicesSlaReport.php b/library/Icingadb/ProvidedHook/CreateServicesSlaReport.php
new file mode 100644
index 0000000..a65b54e
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/CreateServicesSlaReport.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Icingadb\Hook\ServicesDetailExtensionHook;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\ValidHtml;
+use ipl\I18n\Translation;
+use ipl\Orm\Query;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+class CreateServicesSlaReport extends ServicesDetailExtensionHook
+{
+ use Translation;
+
+ public function getHtmlForObjects(Query $services): ValidHtml
+ {
+ if (Auth::getInstance()->hasPermission('reporting/reports')) {
+ $filter = QueryString::render($this->getBaseFilter());
+ return (new HtmlDocument())
+ ->addHtml(Html::tag('h2', $this->translate('Reporting')))
+ ->addHtml(new Link(
+ $this->translate('Create Service SLA Report'),
+ Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'service']),
+ [
+ 'data-icinga-modal' => true,
+ 'data-no-icinga-ajax' => true
+ ]
+ ));
+ }
+
+ return new HtmlDocument();
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/IcingaHealth.php b/library/Icingadb/ProvidedHook/IcingaHealth.php
new file mode 100644
index 0000000..54e22c7
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/IcingaHealth.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Model\Instance;
+use ipl\Web\Url;
+
+class IcingaHealth extends HealthHook
+{
+ use Database;
+
+ /** @var Instance */
+ protected $instance;
+
+ public function getName(): string
+ {
+ return 'Icinga DB';
+ }
+
+ public function getUrl(): Url
+ {
+ return Url::fromPath('icingadb/health');
+ }
+
+ public function checkHealth()
+ {
+ $instance = $this->getInstance();
+
+ if ($instance === null) {
+ $this->setState(self::STATE_UNKNOWN);
+ $this->setMessage(t(
+ 'Icinga DB is not running or not writing into the database'
+ . ' (make sure the icinga feature "icingadb" is enabled)'
+ ));
+ } elseif ($instance->heartbeat->getTimestamp() < time() - 60) {
+ $this->setState(self::STATE_CRITICAL);
+ $this->setMessage(t(
+ 'Icinga DB is not running or not writing into the database'
+ . ' (make sure the icinga feature "icingadb" is enabled)'
+ ));
+ } else {
+ $this->setState(self::STATE_OK);
+ $this->setMessage(t('Icinga DB is running and writing into the database'));
+ $warningMessages = [];
+
+ if (! $instance->icinga2_active_host_checks_enabled) {
+ $this->setState(self::STATE_WARNING);
+ $warningMessages[] = t('Active host checks are disabled');
+ }
+
+ if (! $instance->icinga2_active_service_checks_enabled) {
+ $this->setState(self::STATE_WARNING);
+ $warningMessages[] = t('Active service checks are disabled');
+ }
+
+ if (! $instance->icinga2_notifications_enabled) {
+ $this->setState(self::STATE_WARNING);
+ $warningMessages[] = t('Notifications are disabled');
+ }
+
+ if ($this->getState() === self::STATE_WARNING) {
+ $this->setMessage(implode("; ", $warningMessages));
+ }
+ }
+
+ if ($instance !== null) {
+ $this->setMetrics([
+ 'heartbeat' => $instance->heartbeat->getTimestamp(),
+ 'responsible' => $instance->responsible,
+ 'icinga2_active_host_checks_enabled' => $instance->icinga2_active_host_checks_enabled,
+ 'icinga2_active_service_checks_enabled' => $instance->icinga2_active_service_checks_enabled,
+ 'icinga2_event_handlers_enabled' => $instance->icinga2_event_handlers_enabled,
+ 'icinga2_flap_detection_enabled' => $instance->icinga2_flap_detection_enabled,
+ 'icinga2_notifications_enabled' => $instance->icinga2_notifications_enabled,
+ 'icinga2_performance_data_enabled' => $instance->icinga2_performance_data_enabled,
+ 'icinga2_start_time' => $instance->icinga2_start_time->getTimestamp(),
+ 'icinga2_version' => $instance->icinga2_version,
+ 'endpoint' => ['name' => $instance->endpoint->name]
+ ]);
+ }
+ }
+
+ /**
+ * Get an Icinga DB instance
+ *
+ * @return ?Instance
+ */
+ protected function getInstance()
+ {
+ if ($this->instance === null) {
+ $this->instance = Instance::on($this->getDb())
+ ->with('endpoint')
+ ->columns([
+ 'heartbeat',
+ 'responsible',
+ 'icinga2_active_host_checks_enabled',
+ 'icinga2_active_service_checks_enabled',
+ 'icinga2_event_handlers_enabled',
+ 'icinga2_flap_detection_enabled',
+ 'icinga2_notifications_enabled',
+ 'icinga2_performance_data_enabled',
+ 'icinga2_start_time',
+ 'icinga2_version',
+ 'endpoint.name'
+ ])
+ ->first();
+ }
+
+ return $this->instance;
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/RedisHealth.php b/library/Icingadb/ProvidedHook/RedisHealth.php
new file mode 100644
index 0000000..1471aba
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/RedisHealth.php
@@ -0,0 +1,55 @@
+<?php
+
+// Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Icingadb\ProvidedHook;
+
+use Exception;
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Common\IcingaRedis;
+use Icinga\Module\Icingadb\Model\Instance;
+
+class RedisHealth extends HealthHook
+{
+ use Database;
+
+ public function getName(): string
+ {
+ return 'Icinga Redis';
+ }
+
+ public function checkHealth()
+ {
+ try {
+ $lastIcingaHeartbeat = IcingaRedis::getLastIcingaHeartbeat();
+ if ($lastIcingaHeartbeat === null) {
+ $lastIcingaHeartbeat = time();
+ }
+
+ $instance = Instance::on($this->getDb())->columns('heartbeat')->first();
+
+ if ($instance === null) {
+ $this->setState(self::STATE_UNKNOWN);
+ $this->setMessage(t(
+ 'Can\'t check Icinga Redis: Icinga DB is not running or not writing into the database'
+ . ' (make sure the icinga feature "icingadb" is enabled)'
+ ));
+
+ return;
+ }
+
+ $outdatedDbHeartbeat = $instance->heartbeat->getTimestamp() < time() - 60;
+ if (! $outdatedDbHeartbeat || $instance->heartbeat->getTimestamp() <= $lastIcingaHeartbeat) {
+ $this->setState(self::STATE_OK);
+ $this->setMessage(t('Icinga Redis available and up to date.'));
+ } elseif ($instance->heartbeat->getTimestamp() > $lastIcingaHeartbeat) {
+ $this->setState(self::STATE_CRITICAL);
+ $this->setMessage(t('Icinga Redis outdated. Make sure Icinga 2 is running and connected to Redis.'));
+ }
+ } catch (Exception $e) {
+ $this->setState(self::STATE_CRITICAL);
+ $this->setMessage(sprintf(t("Can't connect to Icinga Redis: %s"), $e->getMessage()));
+ }
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php
new file mode 100644
index 0000000..d9c4f4f
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php
@@ -0,0 +1,68 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Reporting\ReportData;
+use Icinga\Module\Reporting\ReportRow;
+use Icinga\Module\Reporting\Timerange;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter\Rule;
+
+use function ipl\I18n\t;
+
+class HostSlaReport extends SlaReport
+{
+ public function getName()
+ {
+ $name = t('Host SLA');
+ if (Icinga::app()->getModuleManager()->hasEnabled('idoreports')) {
+ $name .= ' (Icinga DB)';
+ }
+
+ return $name;
+ }
+
+ protected function createReportData()
+ {
+ return (new ReportData())
+ ->setDimensions([t('Hostname')])
+ ->setValues([t('SLA in %')]);
+ }
+
+ protected function createReportRow($row)
+ {
+ if ($row->sla === null) {
+ return null;
+ }
+
+ return (new ReportRow())
+ ->setDimensions([$row->display_name])
+ ->setValues([(float) $row->sla]);
+ }
+
+ protected function fetchSla(Timerange $timerange, Rule $filter = null)
+ {
+ $sla = Host::on($this->getDb())
+ ->columns([
+ 'display_name',
+ 'sla' => new Expression(sprintf(
+ "get_sla_ok_percent(%s, NULL, '%s', '%s')",
+ 'host.id',
+ $timerange->getStart()->format('Uv'),
+ $timerange->getEnd()->format('Uv')
+ ))
+ ]);
+
+ $this->applyRestrictions($sla);
+
+ if ($filter !== null) {
+ $sla->filter($filter);
+ }
+
+ return $sla;
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php
new file mode 100644
index 0000000..46a0684
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php
@@ -0,0 +1,72 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Reporting\ReportData;
+use Icinga\Module\Reporting\ReportRow;
+use Icinga\Module\Reporting\Timerange;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter\Rule;
+
+use function ipl\I18n\t;
+
+class ServiceSlaReport extends SlaReport
+{
+ public function getName()
+ {
+ $name = t('Service SLA');
+ if (Icinga::app()->getModuleManager()->hasEnabled('idoreports')) {
+ $name .= ' (Icinga DB)';
+ }
+
+ return $name;
+ }
+
+ protected function createReportData()
+ {
+ return (new ReportData())
+ ->setDimensions([t('Hostname'), t('Service Name')])
+ ->setValues([t('SLA in %')]);
+ }
+
+ protected function createReportRow($row)
+ {
+ if ($row->sla === null) {
+ return null;
+ }
+
+ return (new ReportRow())
+ ->setDimensions([$row->host->display_name, $row->display_name])
+ ->setValues([(float) $row->sla]);
+ }
+
+ protected function fetchSla(Timerange $timerange, Rule $filter = null)
+ {
+ $sla = Service::on($this->getDb())
+ ->columns([
+ 'host.display_name',
+ 'display_name',
+ 'sla' => new Expression(sprintf(
+ "get_sla_ok_percent(%s, %s, '%s', '%s')",
+ 'service.host_id',
+ 'service.id',
+ $timerange->getStart()->format('Uv'),
+ $timerange->getEnd()->format('Uv')
+ ))
+ ]);
+
+ $sla->resetOrderBy()->orderBy('host.display_name')->orderBy('display_name');
+
+ $this->applyRestrictions($sla);
+
+ if ($filter !== null) {
+ $sla->filter($filter);
+ }
+
+ return $sla;
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/Reporting/SlaReport.php b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php
new file mode 100644
index 0000000..8dcc64e
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/SlaReport.php
@@ -0,0 +1,297 @@
+<?php
+
+/* Icinga DB Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use DateInterval;
+use DatePeriod;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Reporting\Hook\ReportHook;
+use Icinga\Module\Reporting\ReportData;
+use Icinga\Module\Reporting\ReportRow;
+use Icinga\Module\Reporting\Timerange;
+use ipl\Html\Form;
+use ipl\Html\Html;
+use ipl\Stdlib\Filter\Rule;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Widget\EmptyState;
+
+use function ipl\I18n\t;
+
+/**
+ * Base class for host and service SLA reports
+ */
+abstract class SlaReport extends ReportHook
+{
+ use Auth;
+ use Database;
+
+ /** @var float If an SLA value is lower than the threshold, it is considered not ok */
+ const DEFAULT_THRESHOLD = 99.5;
+
+ /** @var int The amount of decimal places for the report result */
+ const DEFAULT_REPORT_PRECISION = 2;
+
+ /**
+ * Create and return a {@link ReportData} container
+ *
+ * @return ReportData Container initialized with the expected dimensions and value labels for the specific report
+ */
+ abstract protected function createReportData();
+
+ /**
+ * Create and return a {@link ReportRow}
+ *
+ * @param mixed $row Data for the row
+ *
+ * @return ReportRow|null Row with the dimensions and values for the specific report set according to the data
+ * expected in {@link createRepportData()} or null for no data
+ */
+ abstract protected function createReportRow($row);
+
+ /**
+ * Fetch SLA according to specified time range and filter
+ *
+ * @param Timerange $timerange
+ * @param Rule|null $filter
+ *
+ * @return iterable
+ */
+ abstract protected function fetchSla(Timerange $timerange, Rule $filter = null);
+
+ protected function fetchReportData(Timerange $timerange, array $config = null)
+ {
+ $rd = $this->createReportData();
+ $rows = [];
+
+ $filter = trim((string) $config['filter']) ?: '*';
+ $filter = $filter !== '*' ? QueryString::parse($filter) : null;
+
+ $interval = null;
+ $boundary = null;
+ $format = null;
+ if (isset($config['breakdown']) && $config['breakdown'] !== 'none') {
+ switch ($config['breakdown']) {
+ case 'hour':
+ $interval = new DateInterval('PT1H');
+ $format = 'H:i:s';
+ $boundary = '+1 hour';
+
+ break;
+ case 'day':
+ $interval = new DateInterval('P1D');
+ $format = 'Y-m-d';
+ $boundary = 'tomorrow midnight';
+
+ break;
+ case 'week':
+ $interval = new DateInterval('P1W');
+ $format = 'Y-\WW';
+ $boundary = 'monday next week midnight';
+
+ break;
+ case 'month':
+ $interval = new DateInterval('P1M');
+ $format = 'Y-m';
+ $boundary = 'first day of next month midnight';
+
+ break;
+ }
+
+ $dimensions = $rd->getDimensions();
+ $dimensions[] = ucfirst($config['breakdown']);
+ $rd->setDimensions($dimensions);
+
+ foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) {
+ foreach ($this->fetchSla(new Timerange($start, $end), $filter) as $row) {
+ $row = $this->createReportRow($row);
+
+ if ($row === null) {
+ continue;
+ }
+
+ $dimensions = $row->getDimensions();
+ $dimensions[] = $start->format($format);
+ $row->setDimensions($dimensions);
+
+ $rows[] = $row;
+ }
+ }
+ } else {
+ foreach ($this->fetchSla($timerange, $filter) as $row) {
+ $rows[] = $this->createReportRow($row);
+ }
+ }
+
+ $rd->setRows($rows);
+
+ return $rd;
+ }
+
+ /**
+ * Yield start and end times that recur at the specified interval over the given time range
+ *
+ * @param Timerange $timerange
+ * @param DateInterval $interval
+ * @param string|null $boundary English text datetime description for calculating bounds to get
+ * calendar days, weeks or months instead of relative times according to interval
+ *
+ * @return \Generator
+ */
+ protected function yieldTimerange(Timerange $timerange, DateInterval $interval, $boundary = null)
+ {
+ $start = clone $timerange->getStart();
+ $end = clone $timerange->getEnd();
+ $oneSecond = new DateInterval('PT1S');
+
+ if ($boundary !== null) {
+ $intermediate = (clone $start)->modify($boundary);
+ if ($intermediate < $end) {
+ yield [clone $start, $intermediate->sub($oneSecond)];
+
+ $start->modify($boundary);
+ }
+ }
+
+ $period = new DatePeriod($start, $interval, $end, DatePeriod::EXCLUDE_START_DATE);
+
+ foreach ($period as $date) {
+ /** @var \DateTime $date */
+ yield [$start, (clone $date)->sub($oneSecond)];
+
+ $start = $date;
+ }
+
+ yield [$start, $end];
+ }
+
+ public function initConfigForm(Form $form)
+ {
+ $form->addElement('text', 'filter', [
+ 'label' => t('Filter')
+ ]);
+
+ $form->addElement('select', 'breakdown', [
+ 'label' => t('Breakdown'),
+ 'options' => [
+ 'none' => t('None', 'SLA Report Breakdown'),
+ 'hour' => t('Hour'),
+ 'day' => t('Day'),
+ 'week' => t('Week'),
+ 'month' => t('Month')
+ ]
+ ]);
+
+ $form->addElement('number', 'threshold', [
+ 'label' => t('Threshold'),
+ 'placeholder' => static::DEFAULT_THRESHOLD,
+ 'step' => '0.01',
+ 'min' => '1',
+ 'max' => '100'
+ ]);
+
+ $form->addElement('number', 'sla_precision', [
+ 'label' => t('Amount Decimal Places'),
+ 'placeholder' => static::DEFAULT_REPORT_PRECISION,
+ 'min' => '1',
+ 'max' => '12'
+ ]);
+
+ $form->addElement('checkbox', 'export_total', [
+ 'label' => t('Export Total Averages'),
+ 'description' => t('Export total averages to CSV and JSON'),
+ // Instead of y/n, 0/1 can be implicitly cast to bool which is done where the config is actually used.
+ 'checkedValue' => '1',
+ 'uncheckedValue' => '0'
+ ]);
+ }
+
+ public function getData(Timerange $timerange, array $config = null)
+ {
+ return $this->fetchReportData($timerange, $config);
+ }
+
+ public function getHtml(Timerange $timerange, array $config = null)
+ {
+ $data = $this->getData($timerange, $config);
+
+ if (! count($data)) {
+ return new EmptyState(t('No data found.'));
+ }
+
+ $threshold = isset($config['threshold']) ? (float) $config['threshold'] : static::DEFAULT_THRESHOLD;
+
+ $tableHeaderCells = [];
+
+ foreach ($data->getDimensions() as $dimension) {
+ $tableHeaderCells[] = Html::tag('th', null, $dimension);
+ }
+
+ foreach ($data->getValues() as $value) {
+ $tableHeaderCells[] = Html::tag('th', null, $value);
+ }
+
+ $tableRows = [];
+ $precision = $config['sla_precision'] ?? static::DEFAULT_REPORT_PRECISION;
+
+ foreach ($data->getRows() as $row) {
+ $cells = [];
+
+ foreach ($row->getDimensions() as $dimension) {
+ $cells[] = Html::tag('td', null, $dimension);
+ }
+
+ // We only have one metric
+ $sla = $row->getValues()[0];
+
+ if ($sla < $threshold) {
+ $slaClass = 'nok';
+ } else {
+ $slaClass = 'ok';
+ }
+
+ $cells[] = Html::tag('td', ['class' => "sla-column $slaClass"], round($sla, $precision));
+
+ $tableRows[] = Html::tag('tr', null, $cells);
+ }
+
+ // We only have one average
+ $average = $data->getAverages()[0];
+
+ if ($average < $threshold) {
+ $slaClass = 'nok';
+ } else {
+ $slaClass = 'ok';
+ }
+
+ $total = $this instanceof HostSlaReport
+ ? sprintf(t('Total (%d Hosts)'), $data->count())
+ : sprintf(t('Total (%d Services)'), $data->count());
+
+ $tableRows[] = Html::tag('tr', null, [
+ Html::tag('td', ['colspan' => count($data->getDimensions())], $total),
+ Html::tag('td', ['class' => "sla-column $slaClass"], round($average, $precision))
+ ]);
+
+ $table = Html::tag(
+ 'table',
+ ['class' => 'common-table sla-table'],
+ [
+ Html::tag(
+ 'thead',
+ null,
+ Html::tag(
+ 'tr',
+ null,
+ $tableHeaderCells
+ )
+ ),
+ Html::tag('tbody', null, $tableRows)
+ ]
+ );
+
+ return $table;
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php
new file mode 100644
index 0000000..b09ffb7
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php
@@ -0,0 +1,19 @@
+<?php
+
+/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use Icinga\Module\Icingadb\Hook\Common\TotalSlaReportUtils;
+
+use function ipl\I18n\t;
+
+class TotalHostSlaReport extends HostSlaReport
+{
+ use TotalSlaReportUtils;
+
+ public function getName()
+ {
+ return t('Total Host SLA');
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php b/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php
new file mode 100644
index 0000000..e5ebf57
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php
@@ -0,0 +1,19 @@
+<?php
+
+/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\Reporting;
+
+use Icinga\Module\Icingadb\Hook\Common\TotalSlaReportUtils;
+
+use function ipl\I18n\t;
+
+class TotalServiceSlaReport extends ServiceSlaReport
+{
+ use TotalSlaReportUtils;
+
+ public function getName()
+ {
+ return t('Total Service SLA');
+ }
+}
diff --git a/library/Icingadb/ProvidedHook/X509/Sni.php b/library/Icingadb/ProvidedHook/X509/Sni.php
new file mode 100644
index 0000000..6f20a7d
--- /dev/null
+++ b/library/Icingadb/ProvidedHook/X509/Sni.php
@@ -0,0 +1,55 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\ProvidedHook\X509;
+
+use Generator;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\X509\Hook\SniHook;
+use ipl\Web\Filter\QueryString;
+
+class Sni extends SniHook
+{
+ use Auth;
+ use Database;
+
+ /**
+ * @inheritDoc
+ */
+ public function getHosts(Filter $filter = null): Generator
+ {
+ $this->getDb()->ping();
+
+ $queryHost = Host::on($this->getDb())
+ ->columns([
+ 'host_name' => 'name',
+ 'host_address' => 'address',
+ 'host_address6' => 'address6'
+ ]);
+
+ $this->applyRestrictions($queryHost);
+
+ if ($filter !== null) {
+ $queryString = $filter->toQueryString();
+ $filterCondition = QueryString::parse($queryString);
+ $queryHost->filter($filterCondition);
+ }
+
+ $hosts = $this->getDb()->select($queryHost->assembleSelect());
+
+ /** @var Host $host */
+ foreach ($hosts as $host) {
+ if (! empty($host->host_address)) {
+ yield $host->host_address => $host->host_name;
+ }
+
+ if (! empty($host->host_address6)) {
+ yield $host->host_address6 => $host->host_name;
+ }
+ }
+ }
+}