diff options
Diffstat (limited to '')
20 files changed, 2828 insertions, 0 deletions
diff --git a/library/Toplevelview/Command.php b/library/Toplevelview/Command.php new file mode 100644 index 0000000..95fb47b --- /dev/null +++ b/library/Toplevelview/Command.php @@ -0,0 +1,44 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview; + +use Icinga\Application\Icinga; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\IcingaException; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Cli\Command as IcingaCommand; + +class Command extends IcingaCommand +{ + /** @var MonitoringBackend */ + protected $monitoringBackend; + + public function init() + { + parent::init(); + + if (! extension_loaded('yaml')) { + throw new ConfigurationError('You need the PHP extension "yaml" in order to use TopLevelView'); + } + } + + /** + * Retrieves the Icinga MonitoringBackend + * + * @param string|null $name + * + * @return MonitoringBackend + * @throws IcingaException When monitoring is not enabled + */ + protected function monitoringBackend($name = null) + { + if ($this->monitoringBackend === null) { + if (! Icinga::app()->getModuleManager()->hasEnabled('monitoring')) { + throw new IcingaException('The module "monitoring" must be enabled and configured!'); + } + $this->monitoringBackend = MonitoringBackend::instance($name); + } + return $this->monitoringBackend; + } +} diff --git a/library/Toplevelview/Config/ConfigEmitter.php b/library/Toplevelview/Config/ConfigEmitter.php new file mode 100644 index 0000000..b8caff9 --- /dev/null +++ b/library/Toplevelview/Config/ConfigEmitter.php @@ -0,0 +1,72 @@ +<?php +/* TopLevelView module for Icingaweb2 - Copyright (c) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Config; + +use Icinga\Exception\NotImplementedError; +use stdClass; + +class ConfigEmitter +{ + /** @var array */ + protected $config; + + public function __construct($config) + { + $this->config = $config; + } + + public static function classToArray($obj) + { + $arr = array(); + foreach (get_object_vars($obj) as $k => $v) { + if ($k !== 'children') { + $arr[$k] = $v; + } + } + + // handle children last for visibility + if (property_exists($obj, 'children')) { + $arr['children'] = array(); + foreach ($obj->children as $child) { + // convert each child to an array + $arr['children'][] = static::classToArray($child); + } + } + + return $arr; + } + + public static function fromLegacyTree(stdClass $tree) + { + return new static(static::classToArray($tree)); + } + + public function emitJSON(&$contentType = null) + { + $contentType = 'application/json'; + return json_encode($this->config); + } + + public function emitYAML(&$contentType = null) + { + $contentType = 'application/yaml'; + return yaml_emit($this->config, YAML_UTF8_ENCODING, YAML_LN_BREAK); + } + + public function emitEXPORT(&$contentType = null) + { + $contentType = 'text/plain'; + return var_export($this->config, true); + } + + public function emit($format, &$contentType = null) + { + $funcName = 'emit' . strtoupper($format); + if (method_exists($this, $funcName)) { + return $this->$funcName($contentType); + } else { + throw new NotImplementedError('format "%s" is not implemented to emit!', $format); + } + } +} diff --git a/library/Toplevelview/Legacy/LegacyDbHelper.php b/library/Toplevelview/Legacy/LegacyDbHelper.php new file mode 100644 index 0000000..ae50256 --- /dev/null +++ b/library/Toplevelview/Legacy/LegacyDbHelper.php @@ -0,0 +1,525 @@ +<?php +/* TopLevelView module for Icingaweb2 - Copyright (c) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Legacy; + +use Icinga\Application\Benchmark; +use Icinga\Application\Logger; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use stdClass; +use Zend_Db_Adapter_Pdo_Abstract; +use Zend_Db_Adapter_Pdo_Sqlite; + +class LegacyDbHelper +{ + /** @var Zend_Db_Adapter_Pdo_Abstract */ + protected $db; + + /** @var MonitoringBackend */ + protected $backend; + + /** @var MonitoringBackend */ + protected $oldBackend; + + protected static $idoObjectIds = [ + 'host' => 1, + 'service' => 2, + 'hostgroup' => 3, + ]; + + public function __construct(Zend_Db_Adapter_Pdo_Abstract $db, MonitoringBackend $backend = null) + { + $this->db = $db; + $this->backend = $backend; + } + + public function fetchHierarchies() + { + $query = $this->db->select() + ->from('toplevelview_view_hierarchy AS h', array( + 'id', + )) + ->joinLeft('toplevelview_view AS v', 'v.id = h.view_id', array( + 'name', + 'display_name', + )) + ->where('h.level = ?', 0) + ->where('h.root_id = h.id'); + + return $this->db->fetchAll($query); + } + + /** + * Purges stale object references from the database + * + * Apparently the original editor replaces the tree data, + * but leaves unreferenced objects where the view_id has + * no referenced row in toplevelview_view. + * + * @param bool $noop Only check but don't delete + * + * @return array object types with counts cleaned up + */ + public function cleanupUnreferencedObjects($noop = false) + { + $results = [ + 'host' => 0, + 'hostgroup' => 0, + 'service' => 0 + ]; + + foreach (array_keys($results) as $type) { + $query = $this->db->select() + ->from("toplevelview_${type} AS o", ['id']) + ->joinLeft('toplevelview_view AS v', 'v.id = o.view_id', []) + ->where('v.id IS NULL'); + + Logger::debug("searching for unreferenced %s objects: %s", $type, (string) $query); + + $ids = $this->db->fetchCol($query); + $results[$type] = count($ids); + + if (! $noop) { + Logger::debug("deleting unreferenced %s objects: %s", $type, json_encode($ids)); + $this->db->delete("toplevelview_${type}", sprintf('id IN (%s)', join(', ', $ids))); + } + } + + return $results; + } + + /** + * Migrate object ids from an old MonitoringBackend to a new one + * + * Since data is not stored as names, we need to lookup a name for each id, + * and get the new id from the other backend. + * + * @param bool $noop Do not update the database + * @param bool $removeUnknown Remove objects that are unknown in (new) IDO DB + * + * @return int[] + * @throws IcingaException + * @throws \Zend_Db_Adapter_Exception + */ + public function migrateObjectIds($noop = false, $removeUnknown = false) + { + $result = [ + 'host' => 0, + 'service' => 0, + 'hostgroup' => 0, + ]; + + foreach (array_keys($result) as $type) { + $query = $this->db->select() + ->from("toplevelview_${type}", ['id', "${type}_object_id AS object_id"]); + + Logger::debug("querying stored objects of type %s: %s", $type, (string) $query); + + $objects = []; + + // Load objects indexed by object_id + foreach ($this->db->fetchAll($query) as $row) { + $objects[$row['object_id']] = (object) $row; + } + + // Load names from old DB + $idoObjects = $this->oldBackend->getResource()->select() + ->from('icinga_objects', ['object_id', 'name1', 'name2']) + ->where('objecttype_id', self::$idoObjectIds[$type]); + + // Amend objects with names from old DB + foreach ($idoObjects->fetchAll() as $row) { + $id = $row->object_id; + if (array_key_exists($id, $objects)) { + $idx = $row->name1; + if ($row->name2 !== null) { + $idx .= '!' . $row->name2; + } + + $objects[$id]->name = $idx; + } + } + + // Load names from new DB and index by name + $newObjects = []; + foreach ($this->backend->getResource()->fetchAll($idoObjects) as $row) { + $idx = $row->name1; + if ($row->name2 !== null) { + $idx .= '!' . $row->name2; + } + + $newObjects[$idx] = $row; + } + + // Process all objects and store new id + $errors = 0; + foreach ($objects as $object) { + if (! property_exists($object, 'name')) { + Logger::error("object %s %d has not been found in old IDO", $type, $object->object_id); + $errors++; + } else if (! array_key_exists($object->name, $newObjects)) { + Logger::error("object %s %d '%s' has not been found in new IDO", + $type, $object->object_id, $object->name); + $errors++; + } else { + $object->new_object_id = $newObjects[$object->name]->object_id; + $result[$type]++; + } + } + + if (! $removeUnknown && $errors > 0) { + throw new IcingaException("errors have occurred during IDO id migration - see log"); + } + + if (! $noop) { + foreach ($objects as $object) { + if (property_exists($object, 'new_object_id')) { + $this->db->update( + "toplevelview_${type}", + ["${type}_object_id" => $object->new_object_id], + ["${type}_object_id = ?" => $object->object_id] + ); + } else if ($removeUnknown) { + $this->db->delete( + "toplevelview_${type}", + ["${type}_object_id = ?" => $object->object_id] + ); + } + } + } + } + + return $result; + } + + /** + * @param Zend_Db_Adapter_Pdo_Sqlite $db + * @param string $target + * + * @return Zend_Db_Adapter_Pdo_Sqlite + */ + public function copySqliteDb(Zend_Db_Adapter_Pdo_Sqlite $db, $target) + { + // Lock database for copy + $db->query('PRAGMA locking_mode = EXCLUSIVE'); + $db->query('BEGIN EXCLUSIVE'); + + $file = $db->getConfig()['dbname']; + if (! copy($file, $target)) { + throw new IcingaException("could not copy '%s' to '%s'", $file, $target); + } + + $db->query('COMMIT'); + $db->query('PRAGMA locking_mode = NORMAL'); + + return new Zend_Db_Adapter_Pdo_Sqlite([ + 'dbname' => $target, + ]); + } + + protected function fetchDatabaseHierarchy($root_id) + { + $query = $this->db->select() + ->from('toplevelview_view_hierarchy AS p', array()) + ->joinInner( + 'toplevelview_view_hierarchy AS n', + 'n.root_id = p.root_id AND (n.lft BETWEEN p.lft AND p.rgt) AND n.level >= p.level', + array('id', 'level') + )->joinInner( + 'toplevelview_view AS v', + 'v.id = n.view_id', + array('name', 'display_name') + )->joinLeft( + 'toplevelview_host AS h', + 'h.view_id = v.id', + array('h.host_object_id') + )->joinLeft( + 'toplevelview_service AS s', + 's.view_id = v.id', + array('s.service_object_id') + )->joinLeft( + 'toplevelview_hostgroup AS hg', + 'hg.view_id = v.id', + array('hg.hostgroup_object_id') + )->where( + 'p.id = ?', + $root_id + )->group(array( + 'n.root_id', + 'n.lft', + // 'n.id', + 'h.host_object_id', + 's.service_object_id', + 'hg.hostgroup_object_id', + ))->order(array( + 'n.lft', + 'hg.hostgroup_object_id', + 'h.host_object_id', + 's.service_object_id', + )); + + $nodes = $this->db->fetchAll($query); + + if (empty($nodes)) { + throw new NotFoundError('Could not find tree for root_id %d', $root_id); + } + + return $nodes; + } + + protected function buildTree($root_id, $nodes, &$hosts, &$services, &$hostgroups) + { + /** @var stdClass $tree */ + $tree = null; + /** @var stdClass $currentParent */ + $currentParent = null; + $currentNode = null; + $currentLevel = null; + $currentId = null; + $chain = array(); + + $currentHostId = null; + + $hosts = array(); + $hostgroups = array(); + $services = array(); + + foreach ($nodes as $node) { + $node = (object) $node; + $node->id = (int) $node->id; + $node->level = (int) $node->level; + + if ($currentId === null || $currentId !== $node->id) { + // only add the node once (all hosts, services and hostgroups are attached on the same node) + $chain[$node->level] = $node; + + if ($tree === null || $node->id === $root_id) { + $currentParent = $tree = $node; + + // minor tweak: remove "top level view" from title + $newTitle = preg_replace('/^Top\s*Level\s*View\s*/i', '', $tree->name); + if (strlen($newTitle) > 4) { + $tree->name = $tree->display_name = $newTitle; + } + + // add old default behavior for status + $tree->host_never_unhandled = true; + $tree->notification_periods = true; + $tree->ignored_notification_periods = ['notification_none']; // migration for Director + } elseif ($node->level > $currentLevel) { + // level down + $currentParent = $chain[$node->level - 1]; + + if (! property_exists($currentParent, 'children')) { + $currentParent->children = array(); + } + $currentParent->children[] = $node; + } elseif ($node->level === $currentLevel) { + // same level + $currentParent->children[] = $node; + } elseif ($node->level < $currentLevel) { + // level up + $currentParent = $chain[$node->level - 1]; + $currentParent->children[] = $node; + } + + if ($node->name === $node->display_name) { + unset($node->display_name); + } + + $currentId = $node->id; + $currentNode = $node; + + // clear current host when node changes + $currentHostId = null; + + // remove unused values + unset($node->id); + unset($node->level); + } + + if (property_exists($node, 'host_object_id') + && $node->host_object_id !== null + && $currentHostId !== $node->host_object_id + ) { + $currentHostId = $node->host_object_id; + + $host = new stdClass; + $host->host = 'UNKNOWN_HOST_' . $node->host_object_id; + $host->type = 'host'; + $host->object_id = $node->host_object_id; + + if (! property_exists($currentNode, 'children')) { + $currentNode->children = array(); + } + + $currentNode->children['host_' . $node->host_object_id] = $host; + $hosts[$node->host_object_id][] = $host; + } + unset($currentNode->host_object_id); + + if (property_exists($node, 'service_object_id') && $node->service_object_id !== null) { + $service = new stdClass; + $service->host = 'UNKNOWN_HOST'; + $service->service = 'UNKNOWN_SERVICE_' . $node->service_object_id; + $service->type = 'service'; + $service->object_id = $node->service_object_id; + + if (! property_exists($currentNode, 'children')) { + $currentNode->children = array(); + } + $currentNode->children['hostservice_' . $node->service_object_id] = $service; + $services[$node->service_object_id][] = $service; + } + unset($currentNode->service_object_id); + + if (property_exists($node, 'hostgroup_object_id') && $node->hostgroup_object_id !== null) { + $hostgroup = new stdClass; + $hostgroup->hostgroup = 'UNKNOWN_HOSTGROUP_' . $node->hostgroup_object_id; + $hostgroup->type = 'hostgroup'; + $hostgroup->object_id = $node->hostgroup_object_id; + + if (! property_exists($currentNode, 'children')) { + $currentNode->children = array(); + } + $currentNode->children['hostgroup_' . $node->hostgroup_object_id] = $hostgroup; + $hostgroups[$node->hostgroup_object_id][] = $hostgroup; + } + unset($currentNode->hostgroup_object_id); + } + + return $tree; + } + + public function fetchTree($root_id) + { + Benchmark::measure('fetchTree: begin'); + + $nodes = $this->fetchDatabaseHierarchy($root_id); + + Benchmark::measure('fetchTree: fetchAll done'); + + $tree = $this->buildTree($root_id, $nodes, $hosts, $services, $hostgroups); + + Benchmark::measure('fetchTree: done building tree'); + + if (! empty($hosts)) { + $hostNames = $this->fetchHosts(array_keys($hosts)); + foreach ($hosts as $objectId => $nodes) { + if (array_key_exists($objectId, $hostNames)) { + foreach ($nodes as $node) { + $node->host = $hostNames[$objectId]; + } + } + } + } + + Benchmark::measure('fetchTree: done getting host info'); + + if (! empty($services)) { + $icingaServices = $this->fetchServices(array_keys($services)); + foreach ($services as $objectId => $nodes) { + if (array_key_exists($objectId, $icingaServices)) { + foreach ($nodes as $node) { + $s = $icingaServices[$objectId]; + $node->host = $s->host_name; + $node->service = $s->service_name; + } + } + } + } + + Benchmark::measure('fetchTree: done getting service info'); + + if (! empty($hostgroups)) { + $icingaHostgroups = $this->fetchHostgroups(array_keys($hostgroups)); + foreach ($hostgroups as $objectId => $nodes) { + if (array_key_exists($objectId, $icingaHostgroups)) { + foreach ($nodes as $node) { + $node->hostgroup = $icingaHostgroups[$objectId]; + } + } + } + } + + Benchmark::measure('fetchTree: done getting service info'); + + return $tree; + } + + protected function fetchHosts($ids) + { + return $this->monitoringBackend()->getResource()->select() + ->from('icinga_objects', array( + 'object_id', + 'name1', + )) + ->where('object_id', $ids) + ->where('objecttype_id', 1) + ->fetchPairs(); + } + + protected function fetchServices($ids) + { + $rows = $this->monitoringBackend()->getResource()->select() + ->from('icinga_objects', array( + 'object_id' => 'object_id', + 'host_name' => 'name1', + 'service_name' => 'name2', + )) + ->where('object_id', $ids) + ->where('objecttype_id', 2) + ->fetchAll(); + + $services = array(); + foreach ($rows as $row) { + $services[$row->object_id] = $row; + } + return $services; + } + + protected function fetchHostgroups($ids) + { + return $this->monitoringBackend()->getResource()->select() + ->from('icinga_objects', array( + 'object_id', + 'name1', + )) + ->where('object_id', $ids) + ->where('objecttype_id', 3) + ->fetchPairs(); + } + + protected function monitoringBackend() + { + if ($this->backend === null) { + throw new ProgrammingError('monitoringBackend has not been set at runtime!'); + } + return $this->backend; + } + + /** + * @param MonitoringBackend $oldBackend + * + * @return LegacyDbHelper + */ + public function setOldBackend(MonitoringBackend $oldBackend) + { + $this->oldBackend = $oldBackend; + return $this; + } + + /** + * @param Zend_Db_Adapter_Pdo_Sqlite $db + * + * @return $this + */ + public function setDb($db) + { + $this->db = $db; + return $this; + } +} diff --git a/library/Toplevelview/Monitoring/HostgroupQuery.php b/library/Toplevelview/Monitoring/HostgroupQuery.php new file mode 100644 index 0000000..62cc014 --- /dev/null +++ b/library/Toplevelview/Monitoring/HostgroupQuery.php @@ -0,0 +1,132 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +use Icinga\Module\Monitoring\Backend\Ido\Query\HostgroupQuery as IcingaHostgroupQuery; + +/** + * Patched version of HostgroupQuery + */ +class HostgroupQuery extends IcingaHostgroupQuery +{ + use IgnoredNotificationPeriods; + use Options; + + public function __construct($ds, $columns = null, $options = null) + { + $this->setOptions($options); + parent::__construct($ds, $columns); + } + + public function init() + { + if (($periods = $this->getOption('ignored_notification_periods')) !== null) { + $this->ignoreNotificationPeriods($periods); + } + + $patchedColumnMap = array( + 'servicestatus' => array( + 'service_notifications_enabled' => 'ss.notifications_enabled', + 'service_is_flapping' => 'ss.is_flapping', + 'service_state' => ' + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL + THEN 99 + ELSE CASE WHEN ss.state_type = 1 + THEN ss.current_state + ELSE ss.last_hard_state + END + END', + 'service_handled' => ' + CASE WHEN (ss.problem_has_been_acknowledged + COALESCE(hs.current_state, 0)) > 0 + THEN 1 + ELSE 0 + END', + 'service_handled_wo_host' => ' + CASE WHEN ss.problem_has_been_acknowledged > 0 + THEN 1 + ELSE 0 + END', + 'service_in_downtime' => ' + CASE WHEN (ss.scheduled_downtime_depth = 0) + THEN 0 + ELSE 1 + END', + ), + 'hoststatus' => array( + 'host_notifications_enabled' => 'hs.notifications_enabled', + 'host_is_flapping' => 'hs.is_flapping', + 'host_state' => ' + CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL + THEN 99 + ELSE CASE WHEN hs.state_type = 1 + THEN hs.current_state + ELSE hs.last_hard_state + END + END', + 'host_handled' => ' + CASE WHEN hs.problem_has_been_acknowledged > 0 + THEN 1 + ELSE 0 + END', + 'host_in_downtime' => ' + CASE WHEN (hs.scheduled_downtime_depth = 0) + THEN 0 + ELSE 1 + END', + ), + 'servicenotificationperiod' => array( + 'service_notification_period' => 'ntpo.name1', + 'service_in_notification_period' => ' + CASE WHEN ntpo.object_id IS NULL + THEN 1 + ELSE CASE WHEN ntpr.timeperiod_id IS NOT NULL + THEN 1 + ELSE 0 + END + END', + ), + ); + + foreach ($patchedColumnMap as $table => $columns) { + foreach ($columns as $k => $v) { + $this->columnMap[$table][$k] = $v; + } + } + + parent::init(); + } + + protected function joinServicenotificationperiod() + { + $extraJoinCond = ''; + + if ($this->hasIgnoredNotifications()) { + $extraJoinCond .= $this->db->quoteInto( + ' AND ntpo.name1 NOT IN (?)', + $this->getIgnoredNotificationPeriods() + ); + } + + $this->select->joinLeft( + ['ntp' => $this->prefix . 'timeperiods'], + 'ntp.timeperiod_object_id = s.notification_timeperiod_object_id' + . ' AND ntp.config_type = 1 AND ntp.instance_id = s.instance_id', + [] + ); + $this->select->joinLeft( + ['ntpo' => $this->prefix . 'objects'], + 'ntpo.object_id = s.notification_timeperiod_object_id' . $extraJoinCond, + [] + ); + $this->select->joinLeft( + ['ntpr' => $this->prefix . 'timeperiod_timeranges'], + "ntpr.timeperiod_id = ntp.timeperiod_id + AND ntpr.day = DAYOFWEEK(CURRENT_DATE()) - 1 + AND ntpr.start_sec <= UNIX_TIMESTAMP() - UNIX_TIMESTAMP(CURRENT_DATE()) + AND ntpr.end_sec >= UNIX_TIMESTAMP() - UNIX_TIMESTAMP(CURRENT_DATE()) + ", + [] + ); + } +} diff --git a/library/Toplevelview/Monitoring/Hostgroupsummary.php b/library/Toplevelview/Monitoring/Hostgroupsummary.php new file mode 100644 index 0000000..c89f09c --- /dev/null +++ b/library/Toplevelview/Monitoring/Hostgroupsummary.php @@ -0,0 +1,33 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +use Icinga\Data\ConnectionInterface; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\DataView\Hostgroupsummary as IcingaHostgroupsummary; + +/** + * Patched version of Hostgroupsummary + * + * Just to load a patched version of HostgroupsummaryQuery + */ +class Hostgroupsummary extends IcingaHostgroupsummary +{ + /** @noinspection PhpMissingParentConstructorInspection */ + /** + * @param ConnectionInterface $connection + * @param array|null $columns + * @param array|null $options + * @noinspection PhpMissingParentConstructorInspection + */ + public function __construct( + ConnectionInterface $connection, + array $columns = null, + $options = null + ) { + /** @var MonitoringBackend $connection */ + $this->connection = $connection; + $this->query = new HostgroupsummaryQuery($connection->getResource(), $columns, $options); + } +} diff --git a/library/Toplevelview/Monitoring/HostgroupsummaryQuery.php b/library/Toplevelview/Monitoring/HostgroupsummaryQuery.php new file mode 100644 index 0000000..5390613 --- /dev/null +++ b/library/Toplevelview/Monitoring/HostgroupsummaryQuery.php @@ -0,0 +1,157 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Backend\Ido\Query\HostgroupsummaryQuery as IcingaHostgroupsummaryQuery; +use Zend_Db_Expr; +use Zend_Db_Select; + +/** + * Patched version of HostgroupsummaryQuery + */ +class HostgroupsummaryQuery extends IcingaHostgroupsummaryQuery +{ + use Options; + + public function __construct($ds, $columns = null, $options = null) + { + $this->setOptions($options); + parent::__construct($ds, $columns); + } + + public function init() + { + if ($this->getOption('notification_periods') === true) { + $serviceOutDowntime =# + 'service_notifications_enabled = 1 AND service_in_downtime = 0 AND service_in_notification_period = 1'; + $serviceInDowntime = + '(service_notifications_enabled = 0 OR service_in_downtime = 1 OR service_in_notification_period = 0)'; + } else { + $serviceOutDowntime = 'service_notifications_enabled = 1 AND service_in_downtime = 0'; + $serviceInDowntime = '(service_notifications_enabled = 0 OR service_in_downtime = 1)'; + } + + $hostOutDowntime = 'host_notifications_enabled = 1 AND host_in_downtime = 0'; + $hostInDowntime = '(host_notifications_enabled = 0 OR host_in_downtime = 1)'; + + if ($this->getOption('host_never_unhandled') === true) { + $patchServicesHandled = "(service_handled_wo_host = 1 OR service_is_flapping = 1) AND $serviceOutDowntime"; + $patchServicesUnhandled = "service_handled_wo_host = 0 AND service_is_flapping = 0 AND $serviceOutDowntime"; + } else { + $patchServicesHandled = "(service_handled = 1 OR service_is_flapping = 1) AND $serviceOutDowntime"; + $patchServicesUnhandled = "service_handled = 0 AND service_is_flapping = 0 AND $serviceOutDowntime"; + } + + $patchHostsHandled = "(host_handled = 1 OR host_is_flapping = 1) AND $hostOutDowntime"; + $patchHostsUnhandled = "host_handled = 0 AND host_is_flapping = 0 AND $hostOutDowntime"; + + $patchedColumnMap = array( + 'hostgroupsummary' => array( + 'hosts_down_handled' => + "SUM(CASE WHEN host_state = 1 AND $patchHostsHandled THEN 1 ELSE 0 END)", + 'hosts_down_unhandled' => + "SUM(CASE WHEN host_state = 1 AND $patchHostsUnhandled THEN 1 ELSE 0 END)", + 'hosts_unreachable_handled' => + "SUM(CASE WHEN host_state = 2 AND $patchHostsHandled THEN 1 ELSE 0 END)", + 'hosts_unreachable_unhandled' => + "SUM(CASE WHEN host_state = 2 AND $patchHostsUnhandled THEN 1 ELSE 0 END)", + 'hosts_downtime_handled' => + "SUM(CASE WHEN host_state != 0 AND $hostInDowntime THEN 1 ELSE 0 END)", + 'hosts_downtime_active' => + "SUM(CASE WHEN $hostInDowntime THEN 1 ELSE 0 END)", + 'services_critical_handled' => + "SUM(CASE WHEN service_state = 2 AND $patchServicesHandled THEN 1 ELSE 0 END)", + 'services_critical_unhandled' => + "SUM(CASE WHEN service_state = 2 AND $patchServicesUnhandled THEN 1 ELSE 0 END)", + 'services_unknown_handled' => + "SUM(CASE WHEN service_state = 3 AND $patchServicesHandled THEN 1 ELSE 0 END)", + 'services_unknown_unhandled' => + "SUM(CASE WHEN service_state = 3 AND $patchServicesUnhandled THEN 1 ELSE 0 END)", + 'services_warning_handled' => + "SUM(CASE WHEN service_state = 1 AND $patchServicesHandled THEN 1 ELSE 0 END)", + 'services_warning_unhandled' => + "SUM(CASE WHEN service_state = 1 AND $patchServicesUnhandled THEN 1 ELSE 0 END)", + 'services_downtime_handled' => + "SUM(CASE WHEN service_state != 0 AND $serviceInDowntime THEN 1 ELSE 0 END)", + 'services_downtime_active' => + "SUM(CASE WHEN $serviceInDowntime THEN 1 ELSE 0 END)", + ) + ); + + foreach ($patchedColumnMap as $table => $columns) { + foreach ($columns as $k => $v) { + $this->columnMap[$table][$k] = $v; + } + } + parent::init(); + } + + protected function createSubQuery($queryName, $columns = array()) + { + if ($queryName === 'Hostgroup') { + // use locally patched query + return new HostgroupQuery($this->ds, $columns, $this->options); + } else { + return parent::createSubQuery($queryName, $columns); + } + } + + protected function joinBaseTables() + { + $this->countQuery = $this->createSubQuery( + 'Hostgroup', + array() + ); + $hostColumns = array( + 'hostgroup_alias', + 'hostgroup_name', + 'host_handled', + 'host_notifications_enabled', + 'host_state', + 'host_is_flapping', + 'host_in_downtime', + 'service_handled' => new Zend_Db_Expr('NULL'), + 'service_handled_wo_host' => new Zend_Db_Expr('NULL'), + 'service_state' => new Zend_Db_Expr('NULL'), + 'service_notifications_enabled' => new Zend_Db_Expr('NULL'), + 'service_is_flapping' => new Zend_Db_Expr('NULL'), + 'service_in_downtime' => new Zend_Db_Expr('NULL'), + ); + + $serviceColumns = array( + 'hostgroup_alias', + 'hostgroup_name', + 'host_handled' => new Zend_Db_Expr('NULL'), + 'host_state' => new Zend_Db_Expr('NULL'), + 'host_notifications_enabled' => new Zend_Db_Expr('NULL'), + 'host_is_flapping' => new Zend_Db_Expr('NULL'), + 'host_in_downtime' => new Zend_Db_Expr('NULL'), + 'service_handled', + 'service_handled_wo_host', + 'service_state', + 'service_notifications_enabled', + 'service_is_flapping', + 'service_in_downtime', + ); + + if ($this->getOption('notification_periods') === true) { + $hostColumns['service_in_notification_period'] = new Zend_Db_Expr('NULL'); + $serviceColumns['service_in_notification_period'] = 'service_in_notification_period'; + } + + $hosts = $this->createSubQuery('Hostgroup', $hostColumns); + // ignore empty hostgroups in this subquery + $hosts->setFilter(Filter::expression('ho.object_id', '>', 0)); + $this->subQueries[] = $hosts; + $services = $this->createSubQuery('Hostgroup', $serviceColumns); + // ignore empty hostgroups in this subquery + $services->setFilter(Filter::expression('ho.object_id', '>', 0)); + $this->subQueries[] = $services; + $this->summaryQuery = $this->db->select()->union(array($hosts, $services), Zend_Db_Select::SQL_UNION_ALL); + $this->select->from(array('hostgroupsummary' => $this->summaryQuery), array()); + $this->group(array('hostgroup_name', 'hostgroup_alias')); + $this->joinedVirtualTables['hostgroupsummary'] = true; + } +} diff --git a/library/Toplevelview/Monitoring/IgnoredNotificationPeriods.php b/library/Toplevelview/Monitoring/IgnoredNotificationPeriods.php new file mode 100644 index 0000000..4b9e94c --- /dev/null +++ b/library/Toplevelview/Monitoring/IgnoredNotificationPeriods.php @@ -0,0 +1,49 @@ +<?php +/* Copyright (C) 2019 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +trait IgnoredNotificationPeriods +{ + protected $ignoredNotificationPeriods = []; + + public function ignoreNotificationPeriod($name) + { + $this->ignoredNotificationPeriods[$name] = true; + return $this; + } + + /** + * @param string|array|iterable $list + * + * @return $this + */ + public function ignoreNotificationPeriods($list) + { + if (is_string($list)) { + /** @var string $list */ + $this->ignoredNotificationPeriods[$list] = true; + } else { + foreach ($list as $i) { + $this->ignoredNotificationPeriods[$i] = true; + } + } + + return $this; + } + + public function getIgnoredNotificationPeriods() + { + return array_keys($this->ignoredNotificationPeriods); + } + + public function resetIgnoredNotificationPeriods() + { + $this->ignoredNotificationPeriods = []; + } + + public function hasIgnoredNotifications() + { + return ! empty($this->ignoredNotificationPeriods); + } +} diff --git a/library/Toplevelview/Monitoring/Options.php b/library/Toplevelview/Monitoring/Options.php new file mode 100644 index 0000000..75f739f --- /dev/null +++ b/library/Toplevelview/Monitoring/Options.php @@ -0,0 +1,33 @@ +<?php +/* Copyright (C) 2019 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +trait Options +{ + protected $options = []; + + public function getOption($key) + { + if (array_key_exists($key, $this->options)) { + return $this->options[$key]; + } + + return null; + } + + public function setOptions($options, $flush = false) + { + if ($flush) { + $this->options = []; + } + + if (! empty($options)) { + foreach ($options as $k => $v) { + $this->options[$k] = $v; + } + } + + return $this; + } +} diff --git a/library/Toplevelview/Monitoring/Servicestatus.php b/library/Toplevelview/Monitoring/Servicestatus.php new file mode 100644 index 0000000..e43572f --- /dev/null +++ b/library/Toplevelview/Monitoring/Servicestatus.php @@ -0,0 +1,38 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +use Icinga\Data\ConnectionInterface; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\DataView\Servicestatus as IcingaServiceStatus; + +class Servicestatus extends IcingaServiceStatus +{ + /** @noinspection PhpMissingParentConstructorInspection */ + /** + * @param ConnectionInterface $connection + * @param array|null $columns + * @noinspection PhpMissingParentConstructorInspection + */ + public function __construct(ConnectionInterface $connection, array $columns = null, $options = null) + { + /** @var MonitoringBackend $connection */ + $this->connection = $connection; + $this->query = new ServicestatusQuery($connection->getResource(), $columns, $options); + } + + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array_merge( + parent::getColumns(), + array( + //'service_in_notification_period', + 'service_notification_period', + ) + ); + } +} diff --git a/library/Toplevelview/Monitoring/ServicestatusQuery.php b/library/Toplevelview/Monitoring/ServicestatusQuery.php new file mode 100644 index 0000000..addee2f --- /dev/null +++ b/library/Toplevelview/Monitoring/ServicestatusQuery.php @@ -0,0 +1,87 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Monitoring; + +use Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatusQuery as IcingaServicestatusQuery; + +/** + * Patched version of ServicestatusQuery + */ +class ServicestatusQuery extends IcingaServicestatusQuery +{ + use IgnoredNotificationPeriods; + use Options; + + public function __construct($ds, $columns = null, $options = null) + { + $this->setOptions($options); + parent::__construct($ds, $columns); + } + + public function init() + { + if (($periods = $this->getOption('ignored_notification_periods')) !== null) { + $this->ignoreNotificationPeriods($periods); + } + + $patchedColumnMap = array( + 'servicestatus' => array( + 'service_handled_wo_host' => 'CASE WHEN ss.problem_has_been_acknowledged > 0 THEN 1 ELSE 0 END', + ), + 'servicenotificationperiod' => array( + 'service_notification_period' => 'ntpo.name1', + 'service_in_notification_period' => ' + CASE WHEN ntpo.name1 IS NULL + THEN 1 + ELSE CASE WHEN ntpr.timeperiod_id IS NOT NULL + THEN 1 + ELSE 0 + END + END', + ), + ); + + foreach ($patchedColumnMap as $table => $columns) { + foreach ($columns as $k => $v) { + $this->columnMap[$table][$k] = $v; + } + } + + parent::init(); + } + + protected function joinServicenotificationperiod() + { + $extraJoinCond = ''; + + if ($this->hasIgnoredNotifications()) { + $extraJoinCond .= $this->db->quoteInto( + ' AND ntpo.name1 NOT IN (?)', + $this->getIgnoredNotificationPeriods() + ); + } + + $this->select->joinLeft( + ["ntp" => $this->prefix . 'timeperiods'], + 'ntp.timeperiod_object_id = s.notification_timeperiod_object_id' + . ' AND ntp.config_type = 1 AND ntp.instance_id = s.instance_id', + [] + ); + $this->select->joinLeft( + ['ntpo' => $this->prefix . 'objects'], + 'ntpo.object_id = s.notification_timeperiod_object_id' + . $extraJoinCond, + [] + ); + $this->select->joinLeft( + ['ntpr' => $this->prefix . 'timeperiod_timeranges'], + 'ntpr.timeperiod_id = ntp.timeperiod_id + AND ntpr.day = DAYOFWEEK(CURRENT_DATE()) - 1 + AND ntpr.start_sec < UNIX_TIMESTAMP() - UNIX_TIMESTAMP(CURRENT_DATE()) + AND ntpr.end_sec > UNIX_TIMESTAMP() - UNIX_TIMESTAMP(CURRENT_DATE()) + ', + [] + ); + } +} diff --git a/library/Toplevelview/Tree/TLVHostGroupNode.php b/library/Toplevelview/Tree/TLVHostGroupNode.php new file mode 100644 index 0000000..a6dc7e4 --- /dev/null +++ b/library/Toplevelview/Tree/TLVHostGroupNode.php @@ -0,0 +1,134 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +use Icinga\Application\Benchmark; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Toplevelview\Monitoring\Hostgroupsummary; + +class TLVHostGroupNode extends TLVIcingaNode +{ + protected $type = 'hostgroup'; + + protected $key = 'hostgroup'; + + protected static $titleKey = 'hostgroup'; + + public static function fetch(TLVTree $root) + { + Benchmark::measure('Begin fetching hostgroups'); + + if (! array_key_exists('hostgroup', $root->registeredObjects) or empty($root->registeredObjects['hostgroup'])) { + throw new NotFoundError('No hostgroups registered to fetch!'); + } + + $names = array_keys($root->registeredObjects['hostgroup']); + + $options = []; + foreach (['notification_periods', 'host_never_unhandled', 'ignored_notification_periods'] as $opt) { + $options[$opt] = $root->get($opt); + } + + // Note: this uses a patched version of Hostsgroupsummary / HostgroupsummaryQuery ! + $hostgroups = new Hostgroupsummary( + $root->getBackend(), + array( + 'hostgroup_name', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_total', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_downtime_handled', + 'hosts_downtime_active', + 'hosts_up', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_total', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning_handled', + 'services_warning_unhandled', + 'services_downtime_handled', + 'services_downtime_active', + ), + $options + ); + + $hostgroups->where('hostgroup_name', $names); + + foreach ($hostgroups as $hostgroup) { + $root->registeredObjects['hostgroup'][$hostgroup->hostgroup_name] = $hostgroup; + } + + Benchmark::measure('Finished fetching hostgroups'); + } + + public function getStatus() + { + if ($this->status === null) { + $this->status = $status = new TLVStatus(); + $key = $this->getKey(); + + if (($data = $this->root->getFetched($this->type, $key)) !== null) { + $status->set('total', $data->hosts_total + $data->services_total); + $status->set('ok', $data->hosts_up + $data->services_ok); + + $status->set('critical_handled', $data->services_critical_handled); + $status->set('critical_unhandled', $data->services_critical_unhandled); + + if ($this->getRoot()->get('host_never_unhandled') === true) { + $status->add( + 'critical_handled', + $data->hosts_down_handled + + $data->hosts_unreachable_handled + + $data->hosts_down_unhandled + + $data->hosts_unreachable_unhandled + ); + } else { + $status->add( + 'critical_handled', + $data->hosts_down_handled + + $data->hosts_unreachable_handled + ); + $status->add( + 'critical_unhandled', + $data->hosts_down_unhandled + + $data->hosts_unreachable_unhandled + ); + } + + $status->set('warning_handled', $data->services_warning_handled); + $status->set('warning_unhandled', $data->services_warning_unhandled); + $status->set('unknown_handled', $data->services_unknown_handled); + $status->set('unknown_unhandled', $data->services_unknown_unhandled); + + $status->set( + 'downtime_handled', + $data->hosts_downtime_handled + + $data->services_downtime_handled + ); + $status->set( + 'downtime_active', + $data->hosts_downtime_active + + $data->services_downtime_active + ); + + // extra metadata for view + $status->setMeta('hosts_total', $data->hosts_total); + $status->setMeta( + 'hosts_unhandled', + $data->hosts_down_unhandled + + $data->hosts_unreachable_unhandled + ); + + $status->set('missing', 0); + } else { + $status->add('missing', 1); + } + } + return $this->status; + } +} diff --git a/library/Toplevelview/Tree/TLVHostNode.php b/library/Toplevelview/Tree/TLVHostNode.php new file mode 100644 index 0000000..f03ae47 --- /dev/null +++ b/library/Toplevelview/Tree/TLVHostNode.php @@ -0,0 +1,81 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +use Icinga\Application\Benchmark; +use Icinga\Exception\NotFoundError; + +class TLVHostNode extends TLVIcingaNode +{ + protected $type = 'host'; + + protected $key = 'host'; + + protected static $titleKey = 'host'; + + public static function fetch(TLVTree $root) + { + Benchmark::measure('Begin fetching hosts'); + + if (! array_key_exists('host', $root->registeredObjects) or empty($root->registeredObjects['host'])) { + throw new NotFoundError('No hosts registered to fetch!'); + } + + $names = array_keys($root->registeredObjects['host']); + + $hosts = $root->getBackend()->select() + ->from('hoststatus', array( + 'host_name', + 'host_hard_state', + 'host_handled', + 'host_in_downtime', + 'host_notifications_enabled', + )) + ->where('host_name', $names); + + foreach ($hosts as $host) { + $root->registeredObjects['host'][$host->host_name] = $host; + } + + Benchmark::measure('Finished fetching hosts'); + } + + public function getStatus() + { + if ($this->status === null) { + $this->status = $status = new TLVStatus(); + $key = $this->getKey(); + + if (($data = $this->root->getFetched($this->type, $key)) !== null) { + $status->zero(); + $status->add('total'); + + $state = $data->host_hard_state; + + if ($data->host_in_downtime > 0 || $data->host_notifications_enabled === '0') { + $status->add('downtime_active'); + $state = '10'; + $handled = ''; + } elseif ($data->host_handled === '1' || $this->getRoot()->get('host_never_unhandled') === true) { + $handled = '_handled'; + } else { + $handled = '_unhandled'; + } + + if ($state === '0') { + $status->add('ok'); + } elseif ($state === '1' || $state === '2') { + $status->add('critical' . $handled); + } elseif ($state === '10') { + $status->add('downtime_handled'); + } else { + $status->add('unknown_handled'); + } + } else { + $status->add('missing', 1); + } + } + return $this->status; + } +} diff --git a/library/Toplevelview/Tree/TLVIcingaNode.php b/library/Toplevelview/Tree/TLVIcingaNode.php new file mode 100644 index 0000000..82779b0 --- /dev/null +++ b/library/Toplevelview/Tree/TLVIcingaNode.php @@ -0,0 +1,25 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +use Icinga\Exception\NotImplementedError; + +class TLVIcingaNode extends TLVTreeNode +{ + protected static $canHaveChildren = false; + + /** + * Interface to fetch data for the implementation + * + * Needs to be extended / replaced by class + * + * @param $root + * + * @throws NotImplementedError + */ + public static function fetch(/** @noinspection PhpUnusedParameterInspection */ TLVTree $root) + { + throw new NotImplementedError('fetch() has not been implemented for %s', get_class(new static)); + } +} diff --git a/library/Toplevelview/Tree/TLVServiceNode.php b/library/Toplevelview/Tree/TLVServiceNode.php new file mode 100644 index 0000000..2036714 --- /dev/null +++ b/library/Toplevelview/Tree/TLVServiceNode.php @@ -0,0 +1,143 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +use Icinga\Application\Benchmark; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Toplevelview\Monitoring\Servicestatus; + +class TLVServiceNode extends TLVIcingaNode +{ + protected $type = 'service'; + + protected $key = '{host}!{service}'; + + public function getTitle() + { + return sprintf( + '%s: %s', + $this->get('host'), + $this->get('service') + ); + } + + public function register() + { + // also register host, because that's what we fetch data with + $hostDummy = new TLVHostNode(); + $this->root->registerObject($hostDummy->getType(), $this->get('host'), get_class($hostDummy)); + + // register myself + return parent::register(); + } + + public function getKey() + { + return sprintf('%s!%s', $this->properties['host'], $this->properties['service']); + } + + public static function fetch(TLVTree $root) + { + Benchmark::measure('Begin fetching services'); + + if (! array_key_exists('service', $root->registeredObjects) or empty($root->registeredObjects['service'])) { + throw new NotFoundError('No services registered to fetch!'); + } + + $names = array_keys($root->registeredObjects['host']); + + $columns = array( + 'host_name', + 'service_description', + 'service_hard_state', + 'service_handled', + 'service_handled_wo_host', + 'service_notifications_enabled', + 'service_notification_period', + 'service_is_flapping', + 'service_in_downtime', + ); + + $options = []; + foreach (['notification_periods', 'host_never_unhandled', 'ignored_notification_periods'] as $opt) { + $options[$opt] = $root->get($opt); + } + + if ($root->get('notification_periods') === true) { + $columns[] = 'service_in_notification_period'; + } + + // Note: this uses a patched version of Servicestatus / ServicestatusQuery ! + $services = new Servicestatus($root->getBackend(), $columns, $options); + $services->where('host_name', $names); + + foreach ($services as $service) { + $key = sprintf('%s!%s', $service->host_name, $service->service_description); + if (array_key_exists($key, $root->registeredObjects['service'])) { + $root->registeredObjects['service'][$key] = $service; + } + } + + Benchmark::measure('Finished fetching services'); + } + + public function getStatus() + { + if ($this->status === null) { + $this->status = $status = new TLVStatus(); + $key = $this->getKey(); + + if (($data = $this->root->getFetched($this->type, $key)) !== null) { + $status->zero(); + $status->add('total'); + + $state = $data->service_hard_state; + + if ($this->getRoot()->get('notification_periods') === true) { + $notInPeriod = $data->service_in_notification_period === '0'; + } else { + $notInPeriod = false; + } + + if ($this->getRoot()->get('host_never_unhandled') === true) { + $isHandled = $data->service_handled_wo_host === '1'; + } else { + $isHandled = $data->service_handled === '1'; + } + $isHandled = $isHandled || $data->service_is_flapping === '1'; + + if ($data->service_in_downtime > 0 + || $data->service_notifications_enabled === '0' + || $notInPeriod + ) { + $status->add('downtime_active'); + if ($state !== '0') { + $state = '10'; + } + } + + if ($isHandled) { + $handled = '_handled'; + } else { + $handled = '_unhandled'; + } + + if ($state === '0' || $state === '99') { + $status->add('ok', 1); + } elseif ($state === '1') { + $status->add('warning' . $handled, 1); + } elseif ($state === '2') { + $status->add('critical' . $handled, 1); + } elseif ($state === '10') { + $status->add('downtime_handled'); + } else { + $status->add('unknown' . $handled, 1); + } + } else { + $status->add('missing', 1); + } + } + return $this->status; + } +} diff --git a/library/Toplevelview/Tree/TLVStatus.php b/library/Toplevelview/Tree/TLVStatus.php new file mode 100644 index 0000000..afecc1e --- /dev/null +++ b/library/Toplevelview/Tree/TLVStatus.php @@ -0,0 +1,115 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +class TLVStatus +{ + protected $properties = array( + 'critical_unhandled' => null, + 'critical_handled' => null, + 'warning_unhandled' => null, + 'warning_handled' => null, + 'unknown_unhandled' => null, + 'unknown_handled' => null, + 'downtime_handled' => null, + 'downtime_active' => null, + 'ok' => null, + 'missing' => null, + 'total' => null, + ); + + protected static $statusPriority = array( + 'critical_unhandled', + 'warning_unhandled', + 'unknown_unhandled', // Note: old TLV ignored UNKNOWN basically + 'critical_handled', + 'warning_handled', + 'unknown_handled', + 'ok', + 'downtime_handled', + 'missing', + ); + + protected $meta = array(); + + public function merge(TLVStatus $status) + { + $properties = $status->getProperties(); + foreach (array_keys($this->properties) as $key) { + if ($this->properties[$key] === null) { + $this->properties[$key] = $properties[$key]; + } else { + $this->properties[$key] += $properties[$key]; + } + } + return $this; + } + + public function get($key) + { + return $this->properties[$key]; + } + + public function set($key, $value) + { + $this->properties[$key] = (int) $value; + return $this; + } + + public function getProperties() + { + return $this->properties; + } + + public function add($key, $value = 1) + { + if ($this->properties[$key] === null) { + $this->properties[$key] = 0; + } + $this->properties[$key] += (int) $value; + return $this; + } + + public function zero() + { + foreach (array_keys($this->properties) as $key) { + $this->properties[$key] = 0; + } + return $this; + } + + public function getOverall() + { + foreach (static::$statusPriority as $key) { + if ($this->properties[$key] !== null && $this->properties[$key] > 0) { + return $this->cssFriendly($key); + } + } + return 'missing'; + } + + protected function cssFriendly($key) + { + return str_replace('_', ' ', $key); + } + + public function getMeta($key) + { + if (array_key_exists($key, $this->meta)) { + return $this->meta[$key]; + } else { + return null; + } + } + + public function getAllMeta() + { + return $this->meta; + } + + public function setMeta($key, $value) + { + $this->meta[$key] = $value; + } +} diff --git a/library/Toplevelview/Tree/TLVTree.php b/library/Toplevelview/Tree/TLVTree.php new file mode 100644 index 0000000..817d52c --- /dev/null +++ b/library/Toplevelview/Tree/TLVTree.php @@ -0,0 +1,251 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +use Icinga\Application\Logger; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Toplevelview\Util\Json; +use Icinga\Module\Toplevelview\ViewConfig; +use Icinga\Web\FileCache; +use stdClass; + +class TLVTree extends TLVTreeNode +{ + protected static $titleKey = 'name'; + + public $registeredTypes = array(); + + public $registeredObjects = array(); + + protected $fetchedData = array(); + + protected $fetched = false; + + protected $fetchTime; + + protected $cacheLifetime = 60; + + /** + * @var MonitoringBackend + */ + protected $backend; + + /** + * @var ViewConfig + */ + protected $config; + + public function getById($id) + { + $ids = explode('-', $id); + $currentNode = $this; + + foreach ($ids as $i) { + $children = $currentNode->getChildren(); + if (! empty($children) && array_key_exists($i, $children)) { + $currentNode = $children[$i]; + } else { + throw new NotFoundError( + 'Could not find ID %s after %s for path %s', + $i, + $currentNode->getFullId(), + $id + ); + } + } + + return $currentNode; + } + + /** + * @return ViewConfig + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param ViewConfig $config + * + * @return $this + */ + public function setConfig(ViewConfig $config) + { + $this->config = $config; + return $this; + } + + public function registerObject($type, $name, $class) + { + if (array_key_exists($type, $this->registeredTypes) && $this->registeredTypes[$type] !== $class) { + throw new ProgrammingError( + 'Tried to register the same type by multiple classes: %s - %s - %s', + $type, + $this->registeredTypes[$type], + $class + ); + } + + $this->registeredTypes[$type] = $class; + $this->registeredObjects[$type][$name] = null; + } + + protected function getCacheName() + { + $config = $this->getConfig(); + return sprintf( + '%s-%s.json', + $config->getName(), + $config->getTextChecksum() + ); + } + + protected function loadCache() + { + if (($lifetime = $this->getCacheLifetime()) <= 0) { + return; + } + + $cacheName = $this->getCacheName(); + try { + $cache = FileCache::instance('toplevelview'); + $currentTime = time(); + $newerThan = $currentTime - $lifetime; + + if ($cache->has($cacheName)) { + $cachedData = Json::decode($cache->get($cacheName)); + + if (property_exists($cachedData, 'data') + && $cachedData->data !== null + && property_exists($cachedData, 'ts') + && $cachedData->ts <= $currentTime // too new maybe + && $cachedData->ts > $newerThan // too old + ) { + foreach ($cachedData->data as $type => $objects) { + $this->registeredObjects[$type] = (array) $objects; + $this->fetchedData[$type] = true; + } + + $this->fetchTime = $cachedData->ts; + $this->fetched = true; + } + } + } catch (IcingaException $e) { + Logger::error('Could not load from toplevelview cache %s: %s', $cacheName, $e->getMessage()); + } + } + + protected function storeCache() + { + if (($lifetime = $this->getCacheLifetime()) <= 0) { + return; + } + + $cacheName = $this->getCacheName(); + try { + $cache = FileCache::instance('toplevelview'); + + $cachedData = new stdClass; + $cachedData->ts = $this->fetchTime; + $cachedData->data = $this->registeredObjects; + + $cache->store($cacheName, Json::encode($cachedData)); + } catch (IcingaException $e) { + Logger::error('Could not store to toplevelview cache %s: %s', $cacheName, $e->getMessage()); + } + } + + protected function fetchType($type) + { + if (! array_key_exists($type, $this->registeredTypes)) { + throw new ProgrammingError('Type %s has not been registered', $type); + } + + if (! array_key_exists($type, $this->fetchedData)) { + /** @var TLVIcingaNode $class */ + $class = $this->registeredTypes[$type]; + $class::fetch($this); + $this->fetchedData[$type] = true; + } + + return $this; + } + + protected function ensureFetched() + { + if ($this->fetched !== true) { + $this->loadCache(); + + if ($this->fetched !== true) { + foreach (array_keys($this->registeredTypes) as $type) { + $this->fetchType($type); + } + + $this->fetchTime = time(); + $this->fetched = true; + + $this->storeCache(); + } + } + + return $this; + } + + public function getFetched($type, $key) + { + $this->ensureFetched(); + + if (array_key_exists($key, $this->registeredObjects[$type]) + && $this->registeredObjects[$type][$key] !== null + ) { + return $this->registeredObjects[$type][$key]; + } else { + return null; + } + } + + public function getBackend() + { + return $this->backend; + } + + public function setBackend(MonitoringBackend $backend) + { + $this->backend = $backend; + return $this; + } + + /** + * @return int time + */ + public function getFetchTime() + { + $this->ensureFetched(); + + return $this->fetchTime; + } + + /** + * @return int seconds + */ + public function getCacheLifetime() + { + return $this->cacheLifetime; + } + + /** + * @param int $cacheLifetime In seconds + * + * @return $this + */ + public function setCacheLifetime($cacheLifetime) + { + $this->cacheLifetime = $cacheLifetime; + return $this; + } +} diff --git a/library/Toplevelview/Tree/TLVTreeNode.php b/library/Toplevelview/Tree/TLVTreeNode.php new file mode 100644 index 0000000..fd70e49 --- /dev/null +++ b/library/Toplevelview/Tree/TLVTreeNode.php @@ -0,0 +1,334 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Tree; + +use Icinga\Application\Benchmark; +use Icinga\Data\Tree\TreeNode; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotImplementedError; +use Icinga\Exception\ProgrammingError; + +class TLVTreeNode extends TreeNode +{ + /** + * @var string + */ + protected $type = 'node'; + + protected $key = null; + + /** + * @var TLVTree + */ + protected $root; + + /** + * @var TLVTreeNode + */ + protected $parent; + + /** + * @var string + */ + protected $fullId; + + /** + * @var TLVStatus + */ + protected $status; + + /** + * @var array + */ + protected $properties = array(); + + protected static $canHaveChildren = true; + + /** + * The key which represents the display title + * + * @var string + */ + protected static $titleKey = 'name'; + + /** + * Mapping types to its implementation class + * + * @var array + */ + protected static $typeMap = array( + 'host' => 'Icinga\\Module\\Toplevelview\\Tree\\TLVHostNode', + 'service' => 'Icinga\\Module\\Toplevelview\\Tree\\TLVServiceNode', + 'hostgroup' => 'Icinga\\Module\\Toplevelview\\Tree\\TLVHostGroupNode', + ); + + /** + * Mapping keys to a type + * + * Warning: order is important when keys overlap! + * + * @var array + */ + protected static $typeKeyMap = array( + 'service' => array('host', 'service'), + 'host' => 'host', + 'hostgroup' => 'hostgroup', + ); + + /** + * @param $array + * @param TLVTreeNode|null $parent + * @param TLVTree $root + * + * @return static + * + * @throws NotImplementedError + * @throws ProgrammingError + */ + public static function fromArray($array, TLVTreeNode $parent = null, TLVTree $root = null) + { + if ($root === null) { + Benchmark::measure('Begin loading TLVTree from array'); + } + + // try to detect type + if (! array_key_exists('type', $array)) { + foreach (self::$typeKeyMap as $type => $keys) { + if (! is_array($keys)) { + $keys = array($keys); + } + $matched = false; + foreach ($keys as $k) { + if (array_key_exists($k, $array)) { + $matched = true; + } else { + continue 2; + } + } + // if all keys are present + if ($matched === true) { + $array['type'] = $type; + break; + } + } + } + + if (array_key_exists('type', $array)) { + $type = $array['type']; + if (array_key_exists($type, self::$typeMap)) { + $node = new self::$typeMap[$type]; + $node->type = $type; + } else { + throw new NotImplementedError('Could not find type "%s" for %s', $type, var_export($array, true)); + } + } elseif ($root === null) { + $node = new static; + } else { + $node = new self; + } + + if ($root === null) { + $node->root = true; // is root + $node->parent = null; + $root = $parent = $node; + } elseif ($parent === null) { + throw new ProgrammingError('You must specify the direct parent!'); + } else { + $node->root = $root; + $node->parent = $parent; + } + + foreach ($array as $key => $value) { + if ($key !== 'children') { + $node->properties[$key] = $value; + } elseif (is_array($value)) { // only array values for children + foreach ($value as $i => $child) { + $childNode = self::fromArray($child, $node, $root); + $childNode->id = $i; + $node->appendChild($childNode); + } + } + } + + $node->register(); + + if ($root === $node) { + Benchmark::measure('Finished loading TLVTree from array'); + } + + return $node; + } + + /** + * Retrieve all objects as breadcrumb + * + * @param array $list for recursion + * + * @return TLVTreeNode[] + */ + public function getBreadCrumb(&$list = array()) + { + array_unshift($list, $this); + if ($this->parent !== $this->root) { + $this->parent->getBreadCrumb($list); + } + return $list; + } + + /** + * + * @return mixed|string + */ + public function getFullId() + { + if ($this->fullId === null) { + $id = (string) $this->id; + if ($this->parent !== $this->root) { + $this->fullId = $this->parent->getFullId() . '-' . $id; + } else { + $this->fullId = $id; + } + } + return $this->fullId; + } + + /** + * @return mixed + */ + public function getType() + { + return $this->type; + } + + public function getProperties() + { + return $this->properties; + } + + public function setProperties($array) + { + $this->properties = $array; + return $this; + } + + public function get($key) + { + if (array_key_exists($key, $this->properties)) { + return $this->properties[$key]; + } else { + return null; + } + } + + public function set($key, $value) + { + $this->properties[$key] = $value; + return $this; + } + + public function getTitle() + { + if (array_key_exists(static::$titleKey, $this->properties)) { + return $this->properties[static::$titleKey]; + } else { + return null; + } + } + + public function getKey() + { + if ($this->key === null) { + throw new ProgrammingError('Can not get key for %s', get_class($this)); + } + + if (array_key_exists($this->key, $this->properties)) { + return $this->properties[$this->key]; + } else { + throw new ProgrammingError( + 'Can not retrieve key for %s in %s', + $this->key, + get_class($this) + ); + } + } + + /** + * @return TLVTree + */ + public function getRoot() + { + return $this->root; + } + + /** + * @return TLVTreeNode + */ + public function getParent() + { + return $this->parent; + } + + /** + * @return TLVTreeNode[] + */ + public function getChildren() + { + return parent::getChildren(); + } + + /** + * Append a child node as the last child of this node + * + * @param TreeNode $child The child to append + * + * @return $this + * + * @throws ConfigurationError When node does not allow children + */ + public function appendChild(TreeNode $child) + { + if (static::$canHaveChildren === true) { + $this->children[] = $child; + } else { + throw new ConfigurationError('Can not add children below type %s', $this->type); + } + return $this; + } + + protected function register() + { + if ($this->type !== 'node') { + $this->root->registerObject($this->type, $this->getKey(), get_class($this)); + } + return $this; + } + + /** + * @return TLVStatus + * @throws ProgrammingError + */ + public function getStatus() + { + if (static::$canHaveChildren === true) { + if ($this->status === null) { + $this->status = new TLVStatus; + + $missed = true; + foreach ($this->getChildren() as $child) { + $this->status->merge($child->getStatus()); + $missed = false; + } + + // Note: old TLV does not count an empty branch as missing... + if ($missed) { + $this->status->add('missing', 1); + } + } + + return $this->status; + } else { + throw new ProgrammingError('getStatus() needs to be implemented for %s', get_class($this)); + } + } +} diff --git a/library/Toplevelview/Util/Json.php b/library/Toplevelview/Util/Json.php new file mode 100644 index 0000000..cad6b3e --- /dev/null +++ b/library/Toplevelview/Util/Json.php @@ -0,0 +1,34 @@ +<?php + +namespace Icinga\Module\Toplevelview\Util; + +use Icinga\Exception\Json\JsonEncodeException; +use Icinga\Util\Json as IcingaJson; + +class Json extends IcingaJson +{ + /** + * {@link json_encode()} wrapper + * + * @param mixed $value + * @param int $options + * @param int $depth + * + * @return string + * @throws JsonEncodeException + */ + public static function encode($value, $options = 0, $depth = 512) + { + if (version_compare(phpversion(), '5.4.0', '<')) { + $encoded = json_encode($value); + } elseif (version_compare(phpversion(), '5.5.0', '<')) { + $encoded = json_encode($value, $options); + } else { + $encoded = json_encode($value, $options, $depth); + } + if (json_last_error() !== JSON_ERROR_NONE) { + throw new JsonEncodeException('%s: %s', static::lastErrorMsg(), var_export($value, true)); + } + return $encoded; + } +} diff --git a/library/Toplevelview/ViewConfig.php b/library/Toplevelview/ViewConfig.php new file mode 100644 index 0000000..dc73b7f --- /dev/null +++ b/library/Toplevelview/ViewConfig.php @@ -0,0 +1,488 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview; + +use Icinga\Application\Benchmark; +use Icinga\Application\Icinga; +use Icinga\Exception\InvalidPropertyException; +use Icinga\Exception\NotImplementedError; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Toplevelview\Tree\TLVTree; +use Icinga\Util\DirectoryIterator; +use Icinga\Web\Session; + +class ViewConfig +{ + const FORMAT_YAML = 'yml'; + const SESSION_PREFIX = 'toplevelview_view_'; + + protected $config_dir; + + protected $name; + + protected $format; + + protected $file_path; + + protected $view; + + protected $raw; + + protected $tree; + + protected $hasBeenLoaded = false; + protected $hasBeenLoadedFromSession = false; + + /** + * Content of the file + * + * @var string + */ + protected $text; + + protected $textChecksum; + + /** + * @param $name + * @param string|null $config_dir + * @param string $format + * + * @return static + */ + public static function loadByName($name, $config_dir = null, $format = self::FORMAT_YAML) + { + $object = new static; + $object + ->setName($name) + ->setConfigDir($config_dir) + ->setFormat($format) + ->load(); + + return $object; + } + + /** + * @param string|null $config_dir + * @param string $format + * + * @return static[] + */ + public static function loadAll($config_dir = null, $format = self::FORMAT_YAML) + { + $suffix = '.' . $format; + + $config_dir = static::configDir($config_dir); + $directory = new DirectoryIterator($config_dir, $suffix); + + $views = array(); + foreach ($directory as $name => $path) { + if (is_dir($path)) { + // no not descend and ignore directories + continue; + } + $name = basename($name, $suffix); + $views[$name] = static::loadByName($name, $config_dir, $format); + } + + // try to load from session + $len = strlen(self::SESSION_PREFIX); + foreach (static::session()->getAll() as $k => $v) { + if (substr($k, 0, $len) === self::SESSION_PREFIX) { + $name = substr($k, $len); + if (! array_key_exists($name, $views)) { + $views[$name] = static::loadByName($name, $config_dir, $format); + } + } + } + + ksort($views); + + return $views; + } + + /** + * @return string + */ + public function getFilePath() + { + if ($this->file_path === null) { + if ($this->format === null) { + throw new ProgrammingError('format not set!'); + } + $this->file_path = $this->getConfigDir() . DIRECTORY_SEPARATOR . $this->name . '.' . $this->format; + } + return $this->file_path; + } + + /** + * @param string $file_path + * + * @return $this + */ + public function setFilePath($file_path) + { + $this->file_path = $file_path; + return $this; + } + + /** + * @return $this + */ + public function load() + { + if ($this->text === null) { + $this->loadFromSession(); + } + if ($this->text === null) { + $this->loadFromFile(); + } + return $this; + } + + public function loadFromFile() + { + $file_path = $this->getFilePath(); + $this->text = file_get_contents($file_path); + if ($this->text === false) { + throw new NotReadableError('Could not read file %s', $file_path); + } + $this->view = null; + $this->hasBeenLoadedFromSession = false; + $this->hasBeenLoaded = true; + return $this; + } + + /** + * @return string + */ + public function getText() + { + return $this->text; + } + + public function getTextChecksum() + { + if ($this->textChecksum === null) { + $this->textChecksum = sha1($this->text); + } + return $this->textChecksum; + } + + /** + * @param $text + * + * @return $this + */ + public function setText($text) + { + $this->text = $text; + $this->textChecksum = null; + $this->raw = null; + $this->tree = null; + return $this; + } + + protected function writeFile($path, $content, $mode = '0660') + { + $existing = file_exists($path); + if (file_put_contents($path, $content) === false) { + throw new NotWritableError('Could not save to %s', $path); + } + + if ($existing === false) { + $octalMode = intval($mode, 8); + if ($mode !== null && false === @chmod($path, $octalMode)) { + throw new NotWritableError('Failed to set file mode "%s" on file "%s"', $mode, $path); + } + } + } + + protected function storeBackup($force = false) + { + $backupDir = $this->getConfigBackupDir(); + + $this->ensureConfigDir($backupDir); + + $ts = (string) time(); + $backup = $backupDir . DIRECTORY_SEPARATOR . $ts . '.' . $this->format; + + if (file_exists($backup)) { + throw new ProgrammingError('History file with timestamp already present: %s', $backup); + } + + $existingFile = $this->getFilePath(); + $oldText = file_get_contents($existingFile); + if ($oldText === false) { + throw new NotReadableError('Could not read file %s', $existingFile); + } + + // only save backup if changed or forced + if ($force || $oldText !== $this->text) { + $this->writeFile($backup, $oldText); + } + } + + public function store() + { + $config_dir = $this->getConfigDir(); + $file_path = $this->getFilePath(); + + $this->ensureConfigDir($config_dir); + + // ensure to save history + if (file_exists($file_path)) { + $this->storeBackup(); + } + + $this->writeFile($file_path, $this->text); + + $this->clearSession(); + return $this; + } + + /** + * @return string + * @throws ProgrammingError When dir is not yet set + */ + public function getConfigDir() + { + if ($this->config_dir === null) { + throw new ProgrammingError('config_dir not yet set!'); + } + return $this->config_dir; + } + + /** + * @return string + */ + public function getConfigBackupDir() + { + return $this->getConfigDir() . DIRECTORY_SEPARATOR . $this->name; + } + + /** + * @param string $config_dir + * + * @return $this + * @throws NotReadableError + */ + public function setConfigDir($config_dir = null) + { + $this->config_dir = static::configDir($config_dir); + $this->file_path = null; + return $this; + } + + protected static function ensureConfigDir($path, $mode = '2770') + { + if (! file_exists($path)) { + if (mkdir($path) !== true) { + throw new NotWritableError( + 'Config path did not exit, and it could not be created: %s', + $path + ); + } + + $octalMode = intval($mode, 8); + if ($mode !== null && false === @chmod($path, $octalMode)) { + throw new NotWritableError('Failed to set file mode "%s" on file "%s"', $mode, $path); + } + } + } + + public static function configDir($config_dir = null) + { + $config_dir_module = Icinga::app()->getModuleManager()->getModule('toplevelview')->getConfigDir(); + if ($config_dir === null) { + $config_dir = $config_dir_module . DIRECTORY_SEPARATOR . 'views'; + } + + static::ensureConfigDir($config_dir_module); + static::ensureConfigDir($config_dir); + + return $config_dir; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } + + /** + * @param mixed $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + $this->file_path = null; + return $this; + } + + /** + * @return string + */ + public function getFormat() + { + return $this->format; + } + + /** + * @param string $format + * + * @return $this + */ + public function setFormat($format) + { + $this->format = $format; + $this->file_path = null; + return $this; + } + + public function getMeta($key) + { + $this->ensureParsed(); + if ($key !== 'children' && array_key_exists($key, $this->raw)) { + return $this->raw[$key]; + } else { + return null; + } + } + + public function setMeta($key, $value) + { + if ($key === 'children') { + throw new ProgrammingError('You can not edit children here!'); + } + $this->raw[$key] = $value; + return $this; + } + + public function getMetaData() + { + $this->ensureParsed(); + $data = array(); + foreach ($this->raw as $key => $value) { + if ($key !== 'children') { + $data[$key] = $value; + } + } + return $data; + } + + protected function ensureParsed() + { + if ($this->raw === null) { + Benchmark::measure('Begin parsing YAML document'); + + $text = $this->getText(); + if ($text === null) { + // new ViewConfig + $this->raw = array(); + } elseif ($this->format == self::FORMAT_YAML) { + // TODO: use stdClass instead of Array? + $this->raw = yaml_parse($text); + if (! is_array($this->raw)) { + throw new InvalidPropertyException('Could not parse YAML config!'); + } + } else { + throw new NotImplementedError("Unknown format '%s'", $this->format); + } + + Benchmark::measure('Finished parsing YAML document'); + } + } + + /** + * Loads the Tree for this configuration + * + * @return TLVTree + */ + public function getTree() + { + if ($this->tree === null) { + $this->ensureParsed(); + $this->tree = $tree = TLVTree::fromArray($this->raw); + $tree->setConfig($this); + } + return $this->tree; + } + + protected function getSessionVarName() + { + return self::SESSION_PREFIX . $this->name; + } + + public static function session() + { + // TODO: is this CLI safe? + return Session::getSession(); + } + + public function loadFromSession() + { + if (($sessionConfig = $this->session()->get($this->getSessionVarName())) !== null) { + $this->text = $sessionConfig; + $this->hasBeenLoadedFromSession = true; + $this->hasBeenLoaded = true; + } + return $this; + } + + public function clearSession() + { + $this->session()->delete($this->getSessionVarName()); + } + + public function storeToSession() + { + $this->session()->set($this->getSessionVarName(), $this->text); + } + + /** + * @return bool + */ + public function hasBeenLoadedFromSession() + { + return $this->hasBeenLoadedFromSession; + } + + /** + * @return bool + */ + public function hasBeenLoaded() + { + return $this->hasBeenLoaded; + } + + public function __clone() + { + $this->name = null; + $this->raw = null; + $this->tree = null; + + $this->hasBeenLoaded = false; + $this->hasBeenLoadedFromSession = false; + } + + public function delete() + { + $file_path = $this->getFilePath(); + + $this->clearSession(); + + if (file_exists($file_path)) { + $this->storeBackup(true); + unlink($file_path); + } + + return $this; + } +} diff --git a/library/Toplevelview/Web/Controller.php b/library/Toplevelview/Web/Controller.php new file mode 100644 index 0000000..c010cd7 --- /dev/null +++ b/library/Toplevelview/Web/Controller.php @@ -0,0 +1,53 @@ +<?php +/* Copyright (C) 2017 Icinga Development Team <info@icinga.com> */ + +namespace Icinga\Module\Toplevelview\Web; + +use Icinga\Application\Icinga; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\IcingaException; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Controller as IcingaController; + +class Controller extends IcingaController +{ + /** @var MonitoringBackend */ + protected $monitoringBackend; + + public function init() + { + parent::init(); + + if (! extension_loaded('yaml')) { + throw new ConfigurationError('You need the PHP extension "yaml" in order to use TopLevelView'); + } + } + + /** + * Retrieves the Icinga MonitoringBackend + * + * @param string|null $name + * + * @return MonitoringBackend + * @throws IcingaException When monitoring is not enabled + */ + protected function monitoringBackend($name = null) + { + if ($this->monitoringBackend === null) { + if (! Icinga::app()->getModuleManager()->hasEnabled('monitoring')) { + throw new IcingaException('The module "monitoring" must be enabled and configured!'); + } + $this->monitoringBackend = MonitoringBackend::instance($name); + } + return $this->monitoringBackend; + } + + protected function setViewScript($name, $controller = null) + { + if ($controller !== null) { + $name = sprintf('%s/%s', $controller, $name); + } + $this->_helper->viewRenderer->setNoController(true); + $this->_helper->viewRenderer->setScriptAction($name); + } +} |