diff options
Diffstat (limited to '')
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; + } + } + } +} |