summaryrefslogtreecommitdiffstats
path: root/library/Director/Resolver
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Director/Resolver/CommandUsage.php104
-rw-r--r--library/Director/Resolver/HostServiceBlacklist.php91
-rw-r--r--library/Director/Resolver/IcingaHostObjectResolver.php44
-rw-r--r--library/Director/Resolver/IcingaObjectResolver.php558
-rw-r--r--library/Director/Resolver/OverriddenVarsResolver.php74
-rw-r--r--library/Director/Resolver/OverrideHelper.php38
-rw-r--r--library/Director/Resolver/TemplateTree.php491
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
+
+ */