diff --git a/application/controllers/ServicesetController.php b/application/controllers/ServicesetController.php index dec7de43..93792745 100644 --- a/application/controllers/ServicesetController.php +++ b/application/controllers/ServicesetController.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Controllers; +use Icinga\Data\Filter\Filter; use Icinga\Module\Director\Forms\IcingaServiceSetForm; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaServiceSet; use Icinga\Module\Director\Web\Controller\ObjectController; use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use Icinga\Module\Director\Web\Table\IcingaHostsMatchingFilterTable; use Icinga\Module\Director\Web\Table\IcingaServiceSetHostTable; use Icinga\Module\Director\Web\Table\IcingaServiceSetServiceTable; use gipfl\IcingaWeb2\Link; @@ -82,7 +84,16 @@ class ServicesetController extends ObjectController $set->getObjectName() ); - IcingaServiceSetHostTable::load($set)->renderTo($this); + $table = IcingaServiceSetHostTable::load($set); + if ($table->count()) { + $table->renderTo($this); + } + $filter = $set->get('assign_filter'); + if (\strlen($filter) > 0) { + $this->content()->add( + IcingaHostsMatchingFilterTable::load(Filter::fromQueryString($filter), $this->db()) + ); + } } protected function addServiceSetTabs() diff --git a/library/Director/Resolver/IcingaHostObjectResolver.php b/library/Director/Resolver/IcingaHostObjectResolver.php new file mode 100644 index 00000000..210645f6 --- /dev/null +++ b/library/Director/Resolver/IcingaHostObjectResolver.php @@ -0,0 +1,44 @@ + 'icinga_command', + 'event_command_id' => 'icinga_command', + 'check_period_id' => 'icinga_timeperiod', + 'command_endpoint_id' => 'icinga_endpoint', + 'zone_id' => 'icinga_zone', + ]; + + protected $booleans = [ + 'enable_notifications', + 'enable_active_checks', + 'enable_passive_checks', + 'enable_event_handler', + 'enable_flapping', + 'enable_perfdata', + 'volatile', + ]; +} diff --git a/library/Director/Resolver/IcingaObjectResolver.php b/library/Director/Resolver/IcingaObjectResolver.php new file mode 100644 index 00000000..8a4e9b96 --- /dev/null +++ b/library/Director/Resolver/IcingaObjectResolver.php @@ -0,0 +1,510 @@ +baseTable . ' warming up'); + $this->db = $db; + // Fetch: ignore disabled? + $this->prepareNameMaps(); + $this->templates = []; + foreach ($this->fetchPlainObjects($this->baseTable, 'template') as $template) { + $id = $template->id; + $this->stripIgnoredProperties($template); + // Let's to this only on final objects: + // $this->replaceRelatedNames($object); + // $this->convertBooleans($object); + $this->stripNullProperties($template); + $this->templates[$id] = (array) $template; + } + $this->templateVars = $this->fetchTemplateVars(); + // Using already resolved data, so this is unused right now: + // $this->groupMemberShips = $this->fetchAllGroups(); + $this->resolvedGroupMemberShips = $this->fetchAllResolvedGroups(); + $this->inheritancePaths = $this->fetchInheritancePaths($this->baseTable, 'host_id'); + foreach ($this->inheritancePaths as $path) { + if (! isset($this->resolvedTemplateProperties[$path])) { + $properties = (object) $this->getResolvedProperties($path); + $this->replaceRelatedNames($properties); + $this->convertBooleans($properties); + $this->resolvedTemplateProperties[$path] = $properties; + $this->resolvedTemplateVars[$path] = $this->getResolvedVars($path); + } + } + + Benchmark::measure('Object Resolver for ' . $this->baseTable . ' is ready'); + + // Notes: + // default != null: + // most icinga objects: disabled => n + // Icinga(ScheduledDowntime|TimePeriod)Range: range_type => include, merge_behaviour => set + // IcingaTemplateChoice: min_required => 0, max_allowed => 1 + // IcingaZone: is_global => n + // ImportSource: import_state => unknown + // SyncRule: sync_state => unknown + } + + protected static function addUniqueMembers(&$list, $newMembers) + { + foreach ($newMembers as $member) { + $pos = \array_search($member, $list); + if ($pos !== false) { + unset($list[$pos]); + } + + \array_unshift($list, $member); + } + } + + public function fetchResolvedObjects() + { + $objects = []; + $allVars = $this->fetchNonTemplateVars(); + foreach ($this->fetchPlainObjects($this->baseTable, 'object') as $object) { + $id = $object->id; // id will be stripped + $objects[$id] = $this->enrichObject($object, $allVars); + } + + return $objects; + } + + public function fetchObjectsMatchingFilter(Filter $filter) + { + DynamicApplyMatches::setType($this->getType()); + DynamicApplyMatches::fixFilterColumns($filter); + $objects = []; + $allVars = $this->fetchNonTemplateVars(); + foreach ($this->fetchPlainObjects($this->baseTable, 'object') as $object) { + $id = $object->id; // id will be stripped + if ($filter->matches($object)) { + $objects[$id] = $this->enrichObject($object, $allVars); + } + } + + return $objects; + } + + protected function enrichObject($object, $allVars) + { + $id = $object->id; + $this->stripIgnoredProperties($object); + if (isset($allVars[$id])) { + $vars = $allVars[$id]; + } else { + $vars = []; + } + $vars += $this->getInheritedVarsById($id); + + // There is no merge, +/-, not yet. Unused, as we use resolved groups: + // if (isset($this->groupMemberShips[$id])) { + // $groups = $this->groupMemberShips[$id]; + // } else { + // $groups = $this->getInheritedGroupsById($id); + // } + if (isset($this->resolvedGroupMemberShips[$id])) { + $groups = $this->resolvedGroupMemberShips[$id]; + } else { + $groups = []; + } + + foreach ($this->getInheritedPropertiesById($id) as $property => $value) { + if (! isset($object->$property)) { + $object->$property = $value; + } + } + $this->replaceRelatedNames($object); + $this->convertBooleans($object); + $this->stripNullProperties($object); + if (! empty($vars)) { + $object->vars = (object) $vars; + static::flattenVars($object); + } + if (! empty($groups)) { + $object->groups = $groups; + } + + return $object; + } + + /** + * @param string $baseTable e.g. icinga_host + * @param string $relColumn e.g. host_id + * @return array + */ + protected function fetchInheritancePaths($baseTable, $relColumn) + { + // select host_id, GROUP_CONCAT(parent_host_id ORDER BY weight) FROM icinga_host_inheritance group by host_id; + $query = $this->db->select() + ->from([ + 'oi' => "${baseTable}_inheritance" + ], [ + $relColumn, + "GROUP_CONCAT(parent_$relColumn ORDER BY weight SEPARATOR ',')" + ]) + ->group($relColumn) + ->order("LENGTH(GROUP_CONCAT(parent_$relColumn ORDER BY weight SEPARATOR ','))"); + + // pgsql: ARRAY_TO_STRING(ARRAY_AGG(---), ',') -> order? + + return $this->db->fetchPairs($query); + } + + protected function getInheritedPropertiesById($objectId) + { + if (isset($this->inheritancePaths[$objectId])) { + return $this->getResolvedProperties($this->inheritancePaths[$objectId]); + } else { + return []; + } + } + + protected function getInheritedVarsById($objectId) + { + if (isset($this->inheritancePaths[$objectId])) { + return $this->getResolvedVars($this->inheritancePaths[$objectId]); + } else { + return []; + } + } + + protected function getInheritedGroupsById($objectId) + { + if (isset($this->inheritancePaths[$objectId])) { + return $this->getResolvedGroups($this->inheritancePaths[$objectId]); + } else { + return []; + } + } + + /** + * @param $path + * @return array[] + */ + protected function getResolvedProperties($path) + { + $pos = \strpos($path, ','); + if ($pos === false) { + return $this->templates[$path]; + } else { + $first = \substr($path, 0, $pos); + $parentPath = \substr($path, $pos + 1); + $result = $this->templates[$first] + + $this->getResolvedProperties($parentPath); + unset($result['object_name']); + + return $result; + } + } + + protected function getResolvedVars($path) + { + $pos = \strpos($path, ','); + if ($pos === false) { + if (isset($this->templateVars[$path])) { + return $this->templateVars[$path]; + } else { + return []; + } + } else { + $first = \substr($path, 0, $pos); + $parentPath = \substr($path, $pos + 1); + return $this->getResolvedVars($first) + + $this->getResolvedVars($parentPath); + } + } + + protected function getResolvedGroups($path) + { + $pos = \strpos($path, ','); + if ($pos === false) { + if (isset($this->groupMemberShips[$path])) { + return $this->groupMemberShips[$path]; + } else { + return []; + } + } else { + $first = \substr($path, 0, $pos); + $parentPath = \substr($path, $pos + 1); + $currentGroups = $this->getResolvedVars($first); + + // There is no merging +/-, not yet + if (empty($currentGroups)) { + return $this->getResolvedVars($parentPath); + } else { + return $currentGroups; + } + } + } + + protected function cleanupObjects(&$objects) + { + foreach ($objects as $object) { + $this->stripIgnoredProperties($object); + $this->replaceRelatedNames($object); + $this->convertBooleans($object); + } + } + + protected function fetchPlainObjects($table, $objectType = null) + { + $query = $this->db->select() + ->from(['o' => $table]) + ->order('o.object_name'); + + if ($objectType !== null) { + $query->where('o.object_type = ?', $objectType); + } + + return $this->db->fetchAll($query); + } + + + /** + * @param \stdClass $object + */ + protected function replaceRelatedNames($object) + { + foreach ($this->nameMaps as $property => $map) { + if (\property_exists($object, $property)) { + // Hint: substr strips _id + if ($object->$property === null) { + $object->{\substr($property, 0, -3)} = null; + } else { + $object->{\substr($property, 0, -3)} = $map[$object->$property]; + } + unset($object->$property); + } + } + } + + protected function translateTemplateIdsToNames($ids) + { + $names = []; + foreach ($ids as $id) { + if (isset($this->templates[$id])) { + $names[] = $this->templates[$id]->object_name; + } else { + throw new \RuntimeException("There is no template with ID $id"); + } + } + + return $names; + } + + protected function stripIgnoredProperties($object) + { + foreach ($this->ignoredProperties as $key) { + unset($object->$key); + } + } + + public function prepareNameMaps() + { + // TODO: fetch from dummy Object? How to ignore irrelevant ones like choices? + $relatedNames = []; + foreach ($this->relatedTables as $key => $relatedTable) { + $relatedNames[$key] = $this->fetchRelationMap($this->baseTable, $relatedTable, $key); + } + + $this->nameMaps = $relatedNames; + } + + protected function convertBooleans($object) + { + foreach ($this->booleans as $property) { + if (\property_exists($object, $property) && $object->$property !== null) { + // Hint: substr strips _id + $object->$property = $object->$property === 'y'; + } + } + } + + protected function stripNullProperties($object) + { + foreach (\array_keys((array) $object) as $property) { + if ($object->$property === null) { + unset($object->$property); + } + } + } + + protected function fetchRelationMap($sourceTable, $destinationTable, $property) + { + $query = $this->db->select() + ->from(['d' => $destinationTable], ['d.id', 'd.object_name']) + ->join(['o' => $sourceTable], "d.id = o.$property", []) + ->order('d.object_name'); + + return $this->db->fetchPairs($query); + } + + protected function fetchTemplateVars() + { + $query = $this->prepareVarsQuery()->where('o.object_type = ?', 'template'); + return $this->fetchAndCombineVars($query); + } + + protected function fetchNonTemplateVars() + { + $query = $this->prepareVarsQuery()->where('o.object_type != ?', 'template'); + return $this->fetchAndCombineVars($query); + } + + protected function fetchAndCombineVars($query) + { + $vars = []; + foreach ($this->db->fetchAll($query) as $var) { + $id = $var->object_id; + if (! isset($vars[$id])) { + $vars[$id] = []; + } + if ($var->format === 'json') { + $vars[$id][$var->varname] = \json_decode($var->varvalue); + } else { + $vars[$id][$var->varname] = $var->varvalue; + } + } + + return $vars; + } + + protected function fetchAllGroups() + { + $query = $this->prepareGroupsQuery(); + return $this->fetchAndCombineGroups($query); + } + + protected function fetchAllResolvedGroups() + { + $query = $this->prepareGroupsQuery(true); + return $this->fetchAndCombineGroups($query); + } + + protected function fetchAndCombineGroups($query) + { + $groups = []; + foreach ($this->db->fetchAll($query) as $group) { + $id = $group->object_id; + if (isset($groups[$id])) { + $groups[$id][$group->group_id] = $group->group_name; + } else { + $groups[$id] = [ + $group->group_id => $group->group_name + ]; + } + } + + return $groups; + } + + protected function prepareGroupsQuery($resolved = false) + { + $type = $this->getType(); + $groupsTable = $this->baseTable . 'group'; + $groupMembershipTable = "${groupsTable}_$type"; + if ($resolved) { + $groupMembershipTable .= '_resolved'; + } + $oRef = "${type}_id"; + $gRef = "${type}group_id"; + + return $this->db->select() + ->from(['gm' => $groupMembershipTable], [ + 'object_id' => $oRef, + 'group_id' => $gRef, + 'group_name' => 'g.object_name' + ]) + ->join(['g' => $groupsTable], "g.id = gm.$gRef", []) + ->order("gm.$oRef") + ->order('g.object_name'); + } + + protected function prepareVarsQuery() + { + $table = $this->baseTable . '_var'; + $ref = $this->getType() . '_id'; + return $this->db->select() + ->from(['v' => $table], [ + 'object_id' => $ref, + 'v.varname', + 'v.varvalue', + 'v.format', + // 'v.checksum', + ]) + ->join(['o' => $this->baseTable], "o.id = v.$ref", []) + ->order('o.id') + ->order('v.varname'); + } + + protected function getType() + { + return \preg_replace('/^icinga_/', '', $this->baseTable); + } + + /** + * Helper, flattens all vars of a given object + * + * The object itself will be modified, and the 'vars' property will be + * replaced with corresponding 'vars.whatever' properties + * + * @param $object + * @param string $key + */ + protected static function flattenVars(\stdClass $object, $key = 'vars') + { + if (property_exists($object, $key)) { + foreach ($object->vars as $k => $v) { + if (is_object($v)) { + static::flattenVars($v, $k); + } + $object->{$key . '.' . $k} = $v; + } + unset($object->$key); + } + } +} diff --git a/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php new file mode 100644 index 00000000..5c1a9b93 --- /dev/null +++ b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php @@ -0,0 +1,75 @@ +filter = $filter; + + $table->dataSource = new ArrayDatasource( + (new IcingaHostObjectResolver($db->getDbAdapter())) + ->fetchObjectsMatchingFilter($filter) + ); + + return $table; + } + + public function renderRow($row) + { + return $this::row([ + Link::create( + $row->object_name, + 'director/host', + ['name' => $row->object_name] + ) + ]); + } + + public function getColumnsToBeRendered() + { + return [ + $this->translate('Hostname'), + ]; + } + + protected function getPaginationAdapter() + { + return new SimpleQueryPaginationAdapter($this->getQuery()); + } + + public function getQuery() + { + return $this->prepareQuery(); + } + + protected function fetchQueryRows() + { + return $this->dataSource->fetchAll($this->getQuery()); + } + + protected function prepareQuery() + { + return new SimpleQuery($this->dataSource, ['object_name']); + } +}