summaryrefslogtreecommitdiffstats
path: root/library
diff options
context:
space:
mode:
Diffstat (limited to 'library')
-rw-r--r--library/Toplevelview/Command.php44
-rw-r--r--library/Toplevelview/Config/ConfigEmitter.php72
-rw-r--r--library/Toplevelview/Legacy/LegacyDbHelper.php525
-rw-r--r--library/Toplevelview/Monitoring/HostgroupQuery.php132
-rw-r--r--library/Toplevelview/Monitoring/Hostgroupsummary.php33
-rw-r--r--library/Toplevelview/Monitoring/HostgroupsummaryQuery.php157
-rw-r--r--library/Toplevelview/Monitoring/IgnoredNotificationPeriods.php49
-rw-r--r--library/Toplevelview/Monitoring/Options.php33
-rw-r--r--library/Toplevelview/Monitoring/Servicestatus.php38
-rw-r--r--library/Toplevelview/Monitoring/ServicestatusQuery.php87
-rw-r--r--library/Toplevelview/Tree/TLVHostGroupNode.php134
-rw-r--r--library/Toplevelview/Tree/TLVHostNode.php81
-rw-r--r--library/Toplevelview/Tree/TLVIcingaNode.php25
-rw-r--r--library/Toplevelview/Tree/TLVServiceNode.php143
-rw-r--r--library/Toplevelview/Tree/TLVStatus.php115
-rw-r--r--library/Toplevelview/Tree/TLVTree.php251
-rw-r--r--library/Toplevelview/Tree/TLVTreeNode.php334
-rw-r--r--library/Toplevelview/Util/Json.php34
-rw-r--r--library/Toplevelview/ViewConfig.php488
-rw-r--r--library/Toplevelview/Web/Controller.php53
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);
+ }
+}