diff options
Diffstat (limited to 'library/Director/Objects/GroupMembershipResolver.php')
-rw-r--r-- | library/Director/Objects/GroupMembershipResolver.php | 689 |
1 files changed, 689 insertions, 0 deletions
diff --git a/library/Director/Objects/GroupMembershipResolver.php b/library/Director/Objects/GroupMembershipResolver.php new file mode 100644 index 0000000..f5ef418 --- /dev/null +++ b/library/Director/Objects/GroupMembershipResolver.php @@ -0,0 +1,689 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use InvalidArgumentException; +use LogicException; +use Zend_Db_Select as ZfSelect; + +/** + * Class GroupMembershipResolver + * + * - Fetches all involved assignments + * - Fetch all (or one) object + * - Fetch all (or one) group + */ +abstract class GroupMembershipResolver +{ + /** @var string Object type, 'host', 'service', 'user' or similar */ + protected $type; + + /** @var array */ + protected $existingMappings; + + /** @var array */ + protected $newMappings; + + /** @var Db */ + protected $connection; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** @var IcingaObject[] */ + protected $objects; + + /** @var IcingaObjectGroup[] */ + protected $groups = array(); + + /** @var array */ + protected $staticGroups = array(); + + /** @var bool */ + protected $deferred = false; + + /** @var bool */ + protected $checked = false; + + /** @var bool */ + protected $useTransactions = false; + + protected $groupMap; + + public function __construct(Db $connection) + { + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + } + + /** + * @return $this + * @throws \Zend_Db_Adapter_Exception + */ + public function refreshAllMappings() + { + return $this->clearGroups()->clearObjects()->refreshDb(true); + } + + public function checkDb() + { + if ($this->checked) { + return $this; + } + + if ($this->isDeferred()) { + // ensure we are not working with cached data + IcingaTemplateRepository::clear(); + } + + Benchmark::measure('Rechecking all objects'); + $this->recheckAllObjects($this->getAppliedGroups()); + if (empty($this->objects) && empty($this->groups)) { + Benchmark::measure('Nothing to check, got no qualified object'); + return $this; + } + + Benchmark::measure('Recheck done, loading existing mappings'); + $this->fetchStoredMappings(); + Benchmark::measure('Got stored group mappings'); + + $this->checked = true; + return $this; + } + + /** + * @param bool $force + * @return $this + * @throws \Zend_Db_Adapter_Exception + */ + public function refreshDb($force = false) + { + if ($force || ! $this->isDeferred()) { + $this->checkDb(); + + if (empty($this->objects) && empty($this->groups)) { + Benchmark::measure('Nothing to check, got no qualified object'); + + return $this; + } + + Benchmark::measure('Ready, going to store new mappings'); + $this->storeNewMappings(); + $this->removeOutdatedMappings(); + Benchmark::measure('Updated group mappings in db'); + } + + return $this; + } + + /** + * @param bool $defer + * @return $this + */ + public function defer($defer = true) + { + $this->deferred = $defer; + return $this; + } + + /** + * @param $use + * @return $this + */ + public function setUseTransactions($use) + { + $this->useTransactions = $use; + return $this; + } + + public function getType() + { + if ($this->type === null) { + throw new LogicException(sprintf( + '"type" is required when extending %s, got none in %s', + __CLASS__, + get_class($this) + )); + } + + return $this->type; + } + + /** + * @return bool + */ + public function isDeferred() + { + return $this->deferred; + } + + /** + * @param IcingaObject $object + * @return $this + */ + public function addObject(IcingaObject $object) + { + // Hint: cannot use hasBeenLoadedFromDB, as it is false in onStore() + // for new objects + if (null === ($id = $object->get('id'))) { + return $this; + } + // Disabling for now, how should this work? + // $this->assertBeenLoadedFromDb($object); + if ($this->objects === null) { + $this->objects = []; + } + + if ($object->isTemplate()) { + $this->includeChildObjects($object); + } else { + $this->objects[$id] = $object; + } + + return $this; + } + + /** + * @param IcingaObject[] $objects + * @return $this + */ + public function addObjects(array $objects) + { + foreach ($objects as $object) { + $this->addObject($object); + } + + return $this; + } + + protected function includeChildObjects(IcingaObject $object) + { + $query = $this->db->select() + ->from(['o' => $object->getTableName()]) + ->where('o.object_type = ?', 'object'); + + IcingaObjectFilterHelper::filterByTemplate( + $query, + $object, + 'o', + Db\IcingaObjectFilterHelper::INHERIT_DIRECT_OR_INDIRECT + ); + + foreach ($object::loadAll($this->connection, $query) as $child) { + $this->objects[$child->getProperty('id')] = $child; + } + + return $this; + } + + /** + * @param IcingaObject $object + * @return $this + */ + public function setObject(IcingaObject $object) + { + $this->clearObjects(); + return $this->addObject($object); + } + + /** + * @param IcingaObject[] $objects + * @return $this + */ + public function setObjects(array $objects) + { + $this->clearObjects(); + return $this->addObjects($objects); + } + + /** + * @return $this + */ + public function clearObjects() + { + $this->objects = array(); + return $this; + } + + /** + * @param IcingaObjectGroup $group + * @return $this + */ + public function addGroup(IcingaObjectGroup $group) + { + $this->assertBeenLoadedFromDb($group); + $this->groups[$group->get('id')] = $group; + + $this->checked = false; + + return $this; + } + + /** + * @param IcingaObjectGroup[] $groups + * @return $this + */ + public function addGroups(array $groups) + { + foreach ($groups as $group) { + $this->addGroup($group); + } + + $this->checked = false; + + return $this; + } + + /** + * @param IcingaObjectGroup $group + * @return $this + */ + public function setGroup(IcingaObjectGroup $group) + { + $this->clearGroups(); + return $this->addGroup($group); + } + + /** + * @param array $groups + * @return $this + */ + public function setGroups(array $groups) + { + $this->clearGroups(); + return $this->addGroups($groups); + } + + /** + * @return $this + */ + public function clearGroups() + { + $this->objects = array(); + $this->checked = false; + return $this; + } + + public function getNewMappings() + { + if ($this->newMappings !== null && $this->existingMappings !== null) { + return $this->getDifference($this->newMappings, $this->existingMappings); + } else { + return []; + } + } + + /** + * @throws \Zend_Db_Adapter_Exception + */ + protected function storeNewMappings() + { + $diff = $this->getNewMappings(); + $count = count($diff); + if ($count === 0) { + return; + } + + $db = $this->db; + $this->beginTransaction(); + foreach ($diff as $row) { + $db->insert( + $this->getResolvedTableName(), + $row + ); + } + + $this->commit(); + Benchmark::measure( + sprintf( + 'Stored %d new resolved group memberships', + $count + ) + ); + } + + protected function getGroupId($name) + { + $type = $this->type; + if ($this->groupMap === null) { + $this->groupMap = $this->db->fetchPairs( + $this->db->select()->from("icinga_${type}group", ['object_name', 'id']) + ); + } + + if (array_key_exists($name, $this->groupMap)) { + return $this->groupMap[$name]; + } else { + throw new InvalidArgumentException(sprintf( + 'Unable to lookup the group name for "%s"', + $name + )); + } + } + + public function getOutdatedMappings() + { + if ($this->newMappings !== null && $this->existingMappings !== null) { + return $this->getDifference($this->existingMappings, $this->newMappings); + } else { + return []; + } + } + + protected function removeOutdatedMappings() + { + $diff = $this->getOutdatedMappings(); + $count = count($diff); + if ($count === 0) { + return; + } + + $type = $this->getType(); + $db = $this->db; + $this->beginTransaction(); + foreach ($diff as $row) { + $db->delete( + $this->getResolvedTableName(), + sprintf( + "(${type}group_id = %d AND ${type}_id = %d)", + $row["${type}group_id"], + $row["${type}_id"] + ) + ); + } + + $this->commit(); + Benchmark::measure( + sprintf( + 'Removed %d outdated group memberships', + $count + ) + ); + } + + protected function getDifference(&$left, &$right) + { + $diff = array(); + + $type = $this->getType(); + foreach ($left as $groupId => $objectIds) { + if (array_key_exists($groupId, $right)) { + foreach ($objectIds as $objectId) { + if (! array_key_exists($objectId, $right[$groupId])) { + $diff[] = array( + "${type}group_id" => $groupId, + "${type}_id" => $objectId, + ); + } + } + } else { + foreach ($objectIds as $objectId) { + $diff[] = array( + "${type}group_id" => $groupId, + "${type}_id" => $objectId, + ); + } + } + } + + return $diff; + } + + /** + * This fetches already resolved memberships + */ + protected function fetchStoredMappings() + { + $mappings = array(); + + $type = $this->getType(); + $query = $this->db->select()->from( + array('hgh' => $this->getResolvedTableName()), + array( + 'group_id' => "${type}group_id", + 'object_id' => "${type}_id", + ) + ); + + $this->addMembershipWhere($query, "${type}_id", $this->objects); + $this->addMembershipWhere($query, "${type}group_id", $this->groups); + if (! empty($this->groups)) { + // load staticGroups (we touched here) additionally, so we can compare changes + $this->addMembershipWhere($query, "${type}group_id", $this->staticGroups); + } + + foreach ($this->db->fetchAll($query) as $row) { + $groupId = $row->group_id; + $objectId = $row->object_id; + if (! array_key_exists($groupId, $mappings)) { + $mappings[$groupId] = array(); + } + + $mappings[$groupId][$objectId] = $objectId; + } + + $this->existingMappings = $mappings; + } + + /** + * @param ZfSelect $query + * @param string $column + * @param IcingaObject[]|int[] $objects + * @return ZfSelect + */ + protected function addMembershipWhere(ZfSelect $query, $column, &$objects) + { + if (empty($objects)) { + return $query; + } + + $ids = array(); + foreach ($objects as $k => $object) { + if (is_int($object)) { + $ids[] = $k; + } elseif (is_string($object)) { + $ids[] = (int) $object; + } else { + $ids[] = (int) $object->get('id'); + } + } + + if (count($ids) === 1) { + $query->orWhere($column . ' = ?', $ids[0]); + } else { + $query->orWhere($column . ' IN (?)', $ids); + } + + return $query; + } + + protected function recheckAllObjects($groups) + { + $mappings = []; + $staticGroups = []; + + if ($this->objects === null) { + $objects = $this->fetchAllObjects(); + } else { + $objects = & $this->objects; + } + + $times = array(); + + foreach ($objects as $object) { + if ($object->shouldBeRemoved()) { + continue; + } + if ($object->isTemplate()) { + continue; + } + + $mt = microtime(true); + $id = $object->get('id'); + + DynamicApplyMatches::setType($this->type); + $resolver = DynamicApplyMatches::prepare($object); + foreach ($groups as $groupId => $filter) { + if ($resolver->matchesFilter($filter)) { + if (! array_key_exists($groupId, $mappings)) { + $mappings[$groupId] = []; + } + $mappings[$groupId][$id] = $id; + } + } + + // can only be run reliably when updating for all groups + $groupNames = $object->get('groups'); + if (empty($groupNames)) { + $groupNames = $object->listInheritedGroupNames(); + } + foreach ($groupNames as $name) { + $groupId = $this->getGroupId($name); + if (! array_key_exists($groupId, $mappings)) { + $mappings[$groupId] = []; + } + + $mappings[$groupId][$id] = $id; + $staticGroups[$groupId] = $groupId; + } + + $times[] = (microtime(true) - $mt) * 1000; + } + + $count = count($times); + $min = $max = $avg = 0; + if ($count > 0) { + $min = min($times); + $max = max($times); + $avg = array_sum($times) / $count; + } + + Benchmark::measure(sprintf( + '%sgroup apply recalculated: objects=%d groups=%d min=%d max=%d avg=%d (in ms)', + $this->type, + $count, + count($groups), + $min, + $max, + $avg + )); + + Benchmark::measure('Done with single assignments'); + + $this->newMappings = $mappings; + $this->staticGroups = $staticGroups; + } + + protected function getAppliedGroups() + { + if (empty($this->groups)) { + return $this->fetchAppliedGroups(); + } else { + return $this->buildAppliedGroups(); + } + } + + protected function buildAppliedGroups() + { + $list = array(); + foreach ($this->groups as $id => $group) { + $list[$id] = $group->get('assign_filter'); + } + + return $this->parseFilters($list); + } + + protected function fetchAppliedGroups() + { + $type = $this->getType(); + $query = $this->db->select()->from( + array('hg' => "icinga_${type}group"), + array( + 'id', + 'assign_filter', + ) + )->where("assign_filter IS NOT NULL AND assign_filter != ''"); + + return $this->parseFilters($this->db->fetchPairs($query)); + } + + /** + * Parsing a list of query strings to Filter + * + * @param string[] $list List of query strings + * + * @return Filter[] + */ + protected function parseFilters($list) + { + return array_map(function ($s) { + return Filter::fromQueryString($s); + }, $list); + } + + protected function getTableName() + { + $type = $this->getType(); + return "icinga_${type}group_${type}"; + } + + protected function getResolvedTableName() + { + return $this->getTableName() . '_resolved'; + } + + /** + * @return $this + */ + protected function beginTransaction() + { + if ($this->useTransactions) { + $this->db->beginTransaction(); + } + + return $this; + } + + /** + * @return $this + */ + protected function commit() + { + if ($this->useTransactions) { + $this->db->commit(); + } + + return $this; + } + + /** + * @return IcingaObject[] + */ + protected function getObjects() + { + if ($this->objects === null) { + $this->objects = $this->fetchAllObjects(); + } + + return $this->objects; + } + + protected function fetchAllObjects() + { + return IcingaObject::loadAllByType($this->getType(), $this->connection); + } + + protected function assertBeenLoadedFromDb(IcingaObject $object) + { + if (! is_int($object->get('id')) && ! ctype_digit($object->get('id'))) { + throw new LogicException( + 'Group resolver does not support unstored objects' + ); + } + } +} |