diff options
Diffstat (limited to 'library/Director/Resolver/IcingaObjectResolver.php')
-rw-r--r-- | library/Director/Resolver/IcingaObjectResolver.php | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/library/Director/Resolver/IcingaObjectResolver.php b/library/Director/Resolver/IcingaObjectResolver.php new file mode 100644 index 0000000..540e2c2 --- /dev/null +++ b/library/Director/Resolver/IcingaObjectResolver.php @@ -0,0 +1,558 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Data\AssignFilterHelper; +use Icinga\Module\Director\Objects\DynamicApplyMatches; +use Zend_Db_Adapter_Abstract as ZfDB; + +class IcingaObjectResolver +{ + /** @var ZfDB */ + protected $db; + + protected $nameMaps; + + protected $baseTable = 'not_configured'; + + protected $ignoredProperties = []; + + protected $relatedTables = []; + + protected $booleans = []; + + /** + * @var array[] + */ + protected $templates; + + /** + * @var array[] + */ + protected $resolvedTemplateProperties; + + /** + * @var array + */ + protected $inheritancePaths; + + protected $flatImports = []; + + protected $templateVars; + + protected $resolvedTemplateVars = []; + + protected $groupMemberShips; + + protected $resolvedGroupMemberShips; + + public function __construct(ZfDb $db) + { + // TODO: loop detection. Not critical right now, as this isn't the main resolver + Benchmark::measure('Object Resolver for ' . $this->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); + $this->stripNullProperties($template); + $this->templates[$id] = (array) $template; + } + $this->templateVars = $this->fetchTemplateVars(); + $this->inheritancePaths = $this->fetchInheritancePaths($this->baseTable, 'host_id'); + foreach ($this->inheritancePaths as $path) { + $this->getResolvedImports($path); + } + + // Using already resolved data, so this is unused right now: + // $this->groupMemberShips = $this->fetchAllGroups(); + $this->resolvedGroupMemberShips = $this->fetchAllResolvedGroups(); + + 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 (\array_reverse($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) + { + $filter = clone($filter); + DynamicApplyMatches::setType($this->getType()); + DynamicApplyMatches::fixFilterColumns($filter); + $helper = new AssignFilterHelper($filter); + $objects = []; + $allVars = $this->fetchNonTemplateVars(); + foreach ($this->fetchPlainObjects($this->baseTable, 'object') as $object) { + $id = $object->id; // id will be stripped + $object = $this->enrichObject($object, $allVars); + if ($helper->matches($object)) { + $objects[$id] = $object; + } + } + + 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; + } + + $templates = $this->getTemplateNamesById($id); + if (! empty($templates)) { + $object->templates = \array_reverse($templates); + } + + return $object; + } + + /** + * @param string $baseTable e.g. icinga_host + * @param string $relColumn e.g. host_id + * @return array + */ + protected function fetchInheritancePaths($baseTable, $relColumn) + { + if ($this->db instanceof \Zend_Db_Adapter_Pdo_Pgsql) { + $groupColumn = "ARRAY_TO_STRING(ARRAY_AGG(parent_$relColumn ORDER BY weight), ',')"; + } else { + $groupColumn = "GROUP_CONCAT(parent_$relColumn ORDER BY weight SEPARATOR ',')"; + } + $query = $this->db->select() + ->from([ + 'oi' => "${baseTable}_inheritance" + ], [ + $relColumn, + $groupColumn + ]) + ->group($relColumn) + // Ordering by length increases the possibility to have less cycles afterwards + ->order("LENGTH($groupColumn)"); + + 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 []; + } + } + + protected function getTemplateNamesByID($objectId) + { + if (isset($this->inheritancePaths[$objectId])) { + return $this->translateTemplateIdsToNames( + $this->getResolvedImports($this->inheritancePaths[$objectId]) + ); + } else { + return []; + } + } + + /** + * @param $path + * @return array[] + */ + protected function getResolvedProperties($path) + { + $result = []; + // + adds only non existing members, so let's reverse our templates + foreach ($this->getResolvedImports($path) as $templateId) { + $result += $this->templates[$templateId]; + } + unset($result['object_name']); + + return $result; + } + + protected function getResolvedVars($path) + { + $result = []; + foreach ($this->getResolvedImports($path) as $templateId) { + $result += $this->getTemplateVars($templateId); + } + + return $result; + } + + protected function getTemplateVars($templateId) + { + if (isset($this->templateVars[$templateId])) { + return $this->templateVars[$templateId]; + } else { + return []; + } + } + + 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; + } + } + } + + /** + * Hint: this ships most important (last) imports first + * + * @param $path + * @return array + */ + protected function getResolvedImports($path) + { + if (! isset($this->flatImports[$path])) { + $this->flatImports[$path] = $this->calculateFlatImports($path); + } + + return $this->flatImports[$path]; + } + + protected function calculateFlatImports($path) + { + $imports = \preg_split('/,/', $path); + $ancestors = []; + foreach ($imports as $template) { + if (isset($this->inheritancePaths[$template])) { + $this->addUniqueMembers( + $ancestors, + $this->calculateFlatImports($this->inheritancePaths[$template]) + ); + } + $this->addUniqueMembers($ancestors, [$template]); + } + + return $ancestors; + } + + 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); + } + } +} |