555 lines
16 KiB
PHP
555 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Resolver;
|
|
|
|
use Icinga\Application\Benchmark;
|
|
use Icinga\Data\Filter\Filter;
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
$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);
|
|
}
|
|
}
|
|
}
|