diff options
Diffstat (limited to '')
-rw-r--r-- | library/Director/Resolver/CommandUsage.php | 104 | ||||
-rw-r--r-- | library/Director/Resolver/HostServiceBlacklist.php | 91 | ||||
-rw-r--r-- | library/Director/Resolver/IcingaHostObjectResolver.php | 44 | ||||
-rw-r--r-- | library/Director/Resolver/IcingaObjectResolver.php | 558 | ||||
-rw-r--r-- | library/Director/Resolver/OverriddenVarsResolver.php | 74 | ||||
-rw-r--r-- | library/Director/Resolver/OverrideHelper.php | 38 | ||||
-rw-r--r-- | library/Director/Resolver/TemplateTree.php | 491 |
7 files changed, 1400 insertions, 0 deletions
diff --git a/library/Director/Resolver/CommandUsage.php b/library/Director/Resolver/CommandUsage.php new file mode 100644 index 0000000..7e3e0c5 --- /dev/null +++ b/library/Director/Resolver/CommandUsage.php @@ -0,0 +1,104 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use Icinga\Module\Director\Objects\IcingaCommand; +use InvalidArgumentException; + +class CommandUsage +{ + use TranslationHelper; + + /** @var IcingaCommand */ + protected $command; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** + * CommandUsageTable constructor. + * @param IcingaCommand $command + */ + public function __construct(IcingaCommand $command) + { + if ($command->isTemplate()) { + throw new InvalidArgumentException( + 'CommandUsageTable expects object or external_object, got a template' + ); + } + + $this->command = $command; + $this->db = $command->getDb(); + } + + /** + * @return array + */ + public function getLinks() + { + $name = $this->command->getObjectName(); + $links = []; + $map = [ + 'host' => ['check_command', 'event_command'], + 'service' => ['check_command', 'event_command'], + 'notification' => ['command'], + ]; + $types = [ + 'host' => [ + 'object' => $this->translate('%d Host(s)'), + 'template' => $this->translate('%d Host Template(s)'), + ], + 'service' => [ + 'object' => $this->translate('%d Service(s)'), + 'template' => $this->translate('%d Service Template(s)'), + 'apply' => $this->translate('%d Service Apply Rule(s)'), + ], + 'notification' => [ + 'object' => $this->translate('%d Notification(s)'), + 'template' => $this->translate('%d Notification Template(s)'), + 'apply' => $this->translate('%d Notification Apply Rule(s)'), + ], + ]; + + $urlSuffix = [ + 'object' => '', + 'template' => '/templates', + 'apply' => '/applyrules', + ]; + + foreach ($map as $type => $relations) { + $res = $this->fetchFor($type, $relations, array_keys($types[$type])); + foreach ($types[$type] as $objectType => $caption) { + if ($res->$objectType > 0) { + $suffix = $urlSuffix[$objectType]; + $links[] = Link::create( + sprintf($caption, $res->$objectType), + "director/${type}s$suffix", + ['command' => $name] + ); + } + } + } + + return $links; + } + + protected function fetchFor($table, $rels, $objectTypes) + { + $id = $this->command->getAutoincId(); + + $columns = []; + foreach ($objectTypes as $type) { + $columns[$type] = "COALESCE(SUM(CASE WHEN object_type = '$type' THEN 1 ELSE 0 END), 0)"; + } + $query = $this->db->select()->from("icinga_$table", $columns); + + foreach ($rels as $rel) { + $query->orWhere("${rel}_id = ?", $id); + } + + return $this->db->fetchRow($query); + } +} diff --git a/library/Director/Resolver/HostServiceBlacklist.php b/library/Director/Resolver/HostServiceBlacklist.php new file mode 100644 index 0000000..606855a --- /dev/null +++ b/library/Director/Resolver/HostServiceBlacklist.php @@ -0,0 +1,91 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaService; + +class HostServiceBlacklist +{ + /** @var Db */ + protected $db; + + protected $table = 'icinga_host_service_blacklist'; + + protected $mappings; + + public function __construct(Db $db) + { + $this->db = $db; + } + + protected function loadMappings() + { + $db = $this->db->getDbAdapter(); + $query = $db->select()->from(['hsb' => $this->table], [ + 'host_name' => 'h.object_name', + 'service_id' => 'hsb.service_id' + ])->join( + ['h' => 'icinga_host'], + 'hsb.host_id = h.id', + [] + ); + + $result = []; + foreach ($db->fetchAll($query) as $row) { + if (array_key_exists($row->service_id, $result)) { + $result[$row->service_id][] = $row->host_name; + } else { + $result[$row->service_id] = [$row->host_name]; + } + } + + return $result; + } + + public function preloadMappings() + { + $this->mappings = $this->loadMappings(); + + return $this; + } + + public function getBlacklistedHostnamesForService(IcingaService $service) + { + if ($this->mappings === null) { + return $this->fetchMappingsForService($service); + } else { + return $this->getPreLoadedMappingsForService($service); + } + } + + public function fetchMappingsForService(IcingaService $service) + { + if (! $service->hasBeenLoadedFromDb() || $service->get('id') === null) { + return []; + } + + $db = $this->db->getDbAdapter(); + $query = $db->select()->from(['hsb' => $this->table], [ + 'host_name' => 'h.object_name', + 'service_id' => 'hsb.service_id' + ])->join( + ['h' => 'icinga_host'], + 'hsb.host_id = h.id', + [] + )->where('hsb.service_id = ?', $service->get('id')); + + return $db->fetchCol($query); + } + + public function getPreLoadedMappingsForService(IcingaService $service) + { + if ($this->mappings !== null + && array_key_exists($service->get('id'), $this->mappings) + ) { + return $this->mappings[$service->get('id')]; + } + + return []; + } +} diff --git a/library/Director/Resolver/IcingaHostObjectResolver.php b/library/Director/Resolver/IcingaHostObjectResolver.php new file mode 100644 index 0000000..210645f --- /dev/null +++ b/library/Director/Resolver/IcingaHostObjectResolver.php @@ -0,0 +1,44 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use Zend_Db_Adapter_Abstract as ZfDB; + +class IcingaHostObjectResolver extends IcingaObjectResolver +{ + /** @var ZfDB */ + protected $db; + + protected $nameMaps; + + protected $baseTable = 'icinga_host'; + + protected $ignoredProperties = [ + 'id', + 'object_type', + 'disabled', + 'has_agent', + 'master_should_connect', + 'accept_config', + 'api_key', + 'template_choice_id', + ]; + + protected $relatedTables = [ + 'check_command_id' => '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 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); + } + } +} diff --git a/library/Director/Resolver/OverriddenVarsResolver.php b/library/Director/Resolver/OverriddenVarsResolver.php new file mode 100644 index 0000000..4541244 --- /dev/null +++ b/library/Director/Resolver/OverriddenVarsResolver.php @@ -0,0 +1,74 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaHost; + +class OverriddenVarsResolver +{ + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** @var string */ + protected $overrideVarName; + + public function __construct(Db $connection) + { + $this->overrideVarName = $connection->settings()->get('override_services_varname'); + $this->db = $connection->getDbAdapter(); + } + + public function fetchForHost(IcingaHost $host) + { + $overrides = []; + $parents = $host->listFlatResolvedImportNames(); + if (empty($parents)) { + return $overrides; + } + $query = $this->db->select() + ->from(['hv' => 'icinga_host_var'], [ + 'host_name' => 'h.object_name', + 'varvalue' => 'hv.varvalue', + ]) + ->join( + ['h' => 'icinga_host'], + 'h.id = hv.host_id', + [] + ) + ->where('hv.varname = ?', $this->overrideVarName) + ->where('h.object_name IN (?)', $parents); + + foreach ($this->db->fetchAll($query) as $row) { + if ($row->varvalue === null) { + continue; + } + foreach (Json::decode($row->varvalue) as $serviceName => $vars) { + $overrides[$serviceName][$row->host_name] = $vars; + } + } + + return $overrides; + } + + public function fetchForServiceName(IcingaHost $host, $serviceName) + { + $overrides = $this->fetchForHost($host); + if (isset($overrides[$serviceName])) { + return $overrides[$serviceName]; + } + + return []; + } + + public function fetchVarForServiceName(IcingaHost $host, $serviceName, $varName) + { + $overrides = $this->fetchForHost($host); + if (isset($overrides[$serviceName][$varName])) { + return $overrides[$serviceName][$varName]; + } + + return null; + } +} diff --git a/library/Director/Resolver/OverrideHelper.php b/library/Director/Resolver/OverrideHelper.php new file mode 100644 index 0000000..f911a4f --- /dev/null +++ b/library/Director/Resolver/OverrideHelper.php @@ -0,0 +1,38 @@ +<?php + +namespace Icinga\Module\Director\Resolver; + +use Icinga\Module\Director\Objects\IcingaHost; +use InvalidArgumentException; + +class OverrideHelper +{ + public static function applyOverriddenVars(IcingaHost $host, $serviceName, $properties) + { + static::assertVarsForOverrides($properties); + $current = $host->getOverriddenServiceVars($serviceName); + foreach ($properties as $key => $value) { + if ($key === 'vars') { + foreach ($value as $k => $v) { + $current->$k = $v; + } + } else { + $current->{substr($key, 5)} = $value; + } + } + $host->overrideServiceVars($serviceName, $current); + } + + public static function assertVarsForOverrides($properties) + { + if (empty($properties)) { + return; + } + + foreach ($properties as $key => $value) { + if ($key !== 'vars' && substr($key, 0, 5) !== 'vars.') { + throw new InvalidArgumentException("Only Custom Variables can be set based on Variable Overrides"); + } + } + } +} 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 + + */ |