diff options
Diffstat (limited to 'library/Director/Resolver/TemplateTree.php')
-rw-r--r-- | library/Director/Resolver/TemplateTree.php | 491 |
1 files changed, 491 insertions, 0 deletions
diff --git a/library/Director/Resolver/TemplateTree.php b/library/Director/Resolver/TemplateTree.php new file mode 100644 index 0000000..f8d8fed --- /dev/null +++ b/library/Director/Resolver/TemplateTree.php @@ -0,0 +1,491 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use Icinga\Application\Benchmark; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Exception\NestingError; +use Icinga\Module\Director\Objects\IcingaObject; +use InvalidArgumentException; +use RuntimeException; + +class TemplateTree +{ + protected $connection; + + protected $db; + + protected $parents; + + protected $children; + + protected $rootNodes; + + protected $tree; + + protected $type; + + protected $objectMaps; + + protected $names; + + protected $templateNameToId; + + public function __construct($type, Db $connection) + { + $this->type = $type; + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + } + + public function getType() + { + return $this->type; + } + + public function listParentNamesFor(IcingaObject $object) + { + $id = (int) $object->getProperty('id'); + $this->requireTree(); + + if (array_key_exists($id, $this->parents)) { + return array_values($this->parents[$id]); + } + + $this->requireObjectMaps(); + + $parents = []; + if (array_key_exists($id, $this->objectMaps)) { + foreach ($this->objectMaps[$id] as $pid) { + if (array_key_exists($pid, $this->names)) { + $parents[] = $this->names[$pid]; + } else { + throw new RuntimeException(sprintf( + 'Got invalid parent id %d for %s "%s"', + $pid, + $this->type, + $object->getObjectName() + )); + } + } + } + + return $parents; + } + + protected function loadObjectMaps() + { + $this->requireTree(); + + $map = []; + $db = $this->db; + $type = $this->type; + $table = "icinga_${type}_inheritance"; + + $query = $db->select()->from( + ['i' => $table], + [ + 'object' => "i.${type}_id", + 'parent' => "i.parent_${type}_id", + ] + )->order('i.weight'); + + foreach ($db->fetchAll($query) as $row) { + $id = (int) $row->object; + if (! array_key_exists($id, $map)) { + $map[$id] = []; + } + $map[$id][] = (int) $row->parent; + } + + $this->objectMaps = $map; + } + + public function listParentIdsFor(IcingaObject $object) + { + return array_keys($this->getParentsFor($object)); + } + + public function listAncestorIdsFor(IcingaObject $object) + { + return array_keys($this->getAncestorsFor($object)); + } + + public function listChildIdsFor(IcingaObject $object) + { + return array_keys($this->getChildrenFor($object)); + } + + public function listDescendantIdsFor(IcingaObject $object) + { + return array_keys($this->getDescendantsFor($object)); + } + + public function getParentsFor(IcingaObject $object) + { + // can not use hasBeenLoadedFromDb() when in onStore() + $id = $object->getProperty('id'); + if ($id !== null) { + return $this->getParentsById($object->getProperty('id')); + } else { + throw new RuntimeException( + 'Loading parents for unstored objects has not been implemented yet' + ); + // return $this->getParentsForUnstoredObject($object); + } + } + + public function getAncestorsFor(IcingaObject $object) + { + if ($object->hasBeenModified() + && $object->gotImports() + && $object->imports()->hasBeenModified() + ) { + return $this->getAncestorsForUnstoredObject($object); + } else { + return $this->getAncestorsById($object->getProperty('id')); + } + } + + protected function getAncestorsForUnstoredObject(IcingaObject $object) + { + $this->requireTree(); + $ancestors = []; + foreach ($object->imports() as $import) { + $name = $import->getObjectName(); + if ($import->hasBeenLoadedFromDb()) { + $pid = (int) $import->get('id'); + } else { + if (! array_key_exists($name, $this->templateNameToId)) { + continue; + } + $pid = $this->templateNameToId[$name]; + } + + $this->getAncestorsById($pid, $ancestors); + + // Hint: inheritance order matters + if (false !== ($key = array_search($name, $ancestors))) { + // Note: this used to be just unset($ancestors[$key]), and that + // broke Apply Rules inheriting from Templates with the same name + // in a way that related fields no longer showed up (#1602) + // This new if relaxes this and doesn't unset in case the name + // matches the original object name. However, I'm still unsure why + // this was required at all. + if ($name !== $object->getObjectName()) { + unset($ancestors[$key]); + } + } + $ancestors[$pid] = $name; + } + + return $ancestors; + } + + protected function requireObjectMaps() + { + if ($this->objectMaps === null) { + $this->loadObjectMaps(); + } + } + + public function getParentsById($id) + { + $this->requireTree(); + + if (array_key_exists($id, $this->parents)) { + return $this->parents[$id]; + } + + $this->requireObjectMaps(); + if (array_key_exists($id, $this->objectMaps)) { + $parents = []; + foreach ($this->objectMaps[$id] as $pid) { + $parents[$pid] = $this->names[$pid]; + } + + return $parents; + } else { + return []; + } + } + + /** + * @param $id + * @param $list + * @throws NestingError + */ + protected function assertNotInList($id, &$list) + { + if (array_key_exists($id, $list)) { + $list = array_keys($list); + array_push($list, $id); + + if (is_int($id)) { + throw new NestingError( + 'Loop detected: %s', + implode(' -> ', $this->getNamesForIds($list, true)) + ); + } else { + throw new NestingError( + 'Loop detected: %s', + implode(' -> ', $list) + ); + } + } + } + + protected function getNamesForIds($ids, $ignoreErrors = false) + { + $names = []; + foreach ($ids as $id) { + $names[] = $this->getNameForId($id, $ignoreErrors); + } + + return $names; + } + + protected function getNameForId($id, $ignoreErrors = false) + { + if (! array_key_exists($id, $this->names)) { + if ($ignoreErrors) { + return "id=$id"; + } else { + throw new InvalidArgumentException("Got no name for $id"); + } + } + + return $this->names[$id]; + } + + /** + * @param $id + * @param array $ancestors + * @param array $path + * @return array + * @throws NestingError + */ + public function getAncestorsById($id, &$ancestors = [], $path = []) + { + $path[$id] = true; + foreach ($this->getParentsById($id) as $pid => $name) { + $this->assertNotInList($pid, $path); + $path[$pid] = true; + + $this->getAncestorsById($pid, $ancestors, $path); + unset($path[$pid]); + + // Hint: inheritance order matters + if (false !== ($key = array_search($name, $ancestors))) { + unset($ancestors[$key]); + } + $ancestors[$pid] = $name; + } + unset($path[$id]); + + return $ancestors; + } + + public function getChildrenFor(IcingaObject $object) + { + // can not use hasBeenLoadedFromDb() when in onStore() + $id = $object->getProperty('id'); + if ($id !== null) { + return $this->getChildrenById($id); + } else { + throw new RuntimeException( + 'Loading children for unstored objects has not been implemented yet' + ); + // return $this->getChildrenForUnstoredObject($object); + } + } + + public function getChildrenById($id) + { + $this->requireTree(); + + if (array_key_exists($id, $this->children)) { + return $this->children[$id]; + } else { + return []; + } + } + + public function getDescendantsFor(IcingaObject $object) + { + // can not use hasBeenLoadedFromDb() when in onStore() + $id = $object->getProperty('id'); + if ($id !== null) { + return $this->getDescendantsById($id); + } else { + throw new RuntimeException( + 'Loading descendants for unstored objects has not been implemented yet' + ); + // return $this->getDescendantsForUnstoredObject($object); + } + } + + public function getDescendantsById($id, &$children = [], &$path = []) + { + $path[$id] = true; + foreach ($this->getChildrenById($id) as $pid => $name) { + $this->assertNotInList($pid, $path); + $path[$pid] = true; + $this->getDescendantsById($pid, $children, $path); + unset($path[$pid]); + $children[$pid] = $name; + } + unset($path[$id]); + + return $children; + } + + public function getTree($parentId = null) + { + if ($this->tree === null) { + $this->prepareTree(); + } + + if ($parentId === null) { + return $this->returnFullTree(); + } else { + throw new RuntimeException( + 'Partial tree fetching has not been implemented yet' + ); + // return $this->partialTree($parentId); + } + } + + protected function returnFullTree() + { + $result = $this->rootNodes; + foreach ($result as $id => &$node) { + $this->addChildrenById($id, $node); + } + + return $result; + } + + protected function addChildrenById($pid, array &$base) + { + foreach ($this->getChildrenById($pid) as $id => $name) { + $base['children'][$id] = [ + 'name' => $name, + 'children' => [] + ]; + $this->addChildrenById($id, $base['children'][$id]); + } + } + + protected function prepareTree() + { + Benchmark::measure(sprintf('Prepare "%s" Template Tree', $this->type)); + $templates = $this->fetchTemplates(); + $parents = []; + $rootNodes = []; + $children = []; + $names = []; + foreach ($templates as $row) { + $id = (int) $row->id; + $pid = (int) $row->parent_id; + $names[$id] = $row->name; + if (! array_key_exists($id, $parents)) { + $parents[$id] = []; + } + + if ($row->parent_id === null) { + $rootNodes[$id] = [ + 'name' => $row->name, + 'children' => [] + ]; + continue; + } + + $names[$pid] = $row->parent_name; + $parents[$id][$pid] = $row->parent_name; + + if (! array_key_exists($pid, $children)) { + $children[$pid] = []; + } + + $children[$pid][$id] = $row->name; + } + + $this->parents = $parents; + $this->children = $children; + $this->rootNodes = $rootNodes; + $this->names = $names; + $this->templateNameToId = array_flip($names); + Benchmark::measure(sprintf('"%s" Template Tree ready', $this->type)); + } + + public function fetchObjects() + { + //?? + } + + protected function requireTree() + { + if ($this->parents === null) { + $this->prepareTree(); + } + } + + public function fetchTemplates() + { + $db = $this->db; + $type = $this->type; + $table = "icinga_$type"; + + if ($type === 'command') { + $joinCondition = $db->quoteInto( + "p.id = i.parent_${type}_id", + 'template' + ); + } else { + $joinCondition = $db->quoteInto( + "p.id = i.parent_${type}_id AND p.object_type = ?", + 'template' + ); + } + + $query = $db->select()->from( + ['o' => $table], + [ + 'id' => 'o.id', + 'name' => 'o.object_name', + 'object_type' => 'o.object_type', + 'parent_id' => 'p.id', + 'parent_name' => 'p.object_name', + ] + )->joinLeft( + ['i' => $table . '_inheritance'], + 'o.id = i.' . $type . '_id', + [] + )->joinLeft( + ['p' => $table], + $joinCondition, + [] + )->order('o.id')->order('i.weight'); + + if ($type !== 'command') { + $query->where( + 'o.object_type = ?', + 'template' + ); + } + + return $db->fetchAll($query); + } +} + +/** + * +SELECT o.id, o.object_name AS name, o.object_type, p.id AS parent_id, + p.object_name AS parent_name FROM icinga_service AS o +RIGHT JOIN icinga_service_inheritance AS i ON o.id = i.service_id +RIGHT JOIN icinga_service AS p ON p.id = i.parent_service_id + WHERE (p.object_type = 'template') AND (o.object_type = 'template') + ORDER BY o.id ASC, i.weight ASC + + */ |