diff --git a/library/Director/Objects/DirectorDatafield.php b/library/Director/Objects/DirectorDatafield.php index ced6218ae..f6e7b899d 100644 --- a/library/Director/Objects/DirectorDatafield.php +++ b/library/Director/Objects/DirectorDatafield.php @@ -74,7 +74,8 @@ public function getCategory(): ?DirectorDatafieldCategory if ($this->category) { return $this->category; } elseif ($id = $this->get('category_id')) { - return DirectorDatafieldCategory::loadWithAutoIncId($id, $this->getConnection()); + $this->category = DirectorDatafieldCategory::loadWithAutoIncId($id, $this->getConnection()); + return $this->category; } else { return null; } diff --git a/library/Director/ProvidedHook/Icingadb/CustomVarRenderer.php b/library/Director/ProvidedHook/Icingadb/CustomVarRenderer.php new file mode 100644 index 000000000..fb307a52d --- /dev/null +++ b/library/Director/ProvidedHook/Icingadb/CustomVarRenderer.php @@ -0,0 +1,430 @@ +get('db', 'resource'); + if (! $resourceName) { + throw new ConfigurationError('Cannot identify director db. No resource configured'); + } + + return Db::fromResourceName($resourceName); + } + + public function prefetchForObject(Model $object): bool + { + try { + if ($object instanceof Host) { + $host = $object; + $service = null; + } elseif ($object instanceof Service) { + $host = $object->host; + $service = $object; + } else { + return false; + } + + $db = $this->db(); + + try { + $directorHostObj = IcingaHost::load($host->name, $db); + $directorServiceObj = null; + if ($service !== null) { + $serviceOrigin = ['direct', 'applied', 'inherited', 'service-set']; + $serviceName = $service->name; + $i = 0; + do { + if ($i > 3) { + Logger::error("Failed to find service '%s' on host '%s'", $serviceName, $host->name); + + return false; + } elseif ($serviceOrigin[$i] === 'direct') { + $directorServiceObj = IcingaService::loadOptional([ + 'host_id' => $directorHostObj->get('id'), + 'object_name' => $serviceName + ], $db); + } elseif ($serviceOrigin[$i] === 'inherited') { + $templateResolver = new IcingaTemplateResolver($directorHostObj); + + $parentIds = $templateResolver->listParentIds(); + + $query = $db->getDbAdapter()->select()->from('icinga_service') + ->where('object_name = ?', $serviceName) + ->where('host_id IN (?)', $parentIds); + + $directorServices = IcingaService::loadAll( + $db, + $query, + 'object_name' + ); + + $directorServiceObj = current($directorServices); + } elseif ($serviceOrigin[$i] === 'applied') { + $appliedFilterQuery = IcingaHostAppliedServicesTable::load($directorHostObj)->getQuery(); + + foreach ($appliedFilterQuery->fetchAll() as $appliedService) { + if ($appliedService->name === $serviceName) { + $query = $db->getDbAdapter()->select()->from('icinga_service') + ->where('object_name = ?', $serviceName) + ->where("object_type = 'apply'") + ->where('assign_filter = ?', $appliedService->assign_filter); + + $directorAppliedServices = IcingaService::loadAll( + $db, + $query, + 'object_name' + ); + + $directorServiceObj = current($directorAppliedServices); + + break; + } + } + } elseif ($serviceOrigin[$i] === 'service-set') { + $templateResolver = new IcingaTemplateResolver($directorHostObj); + + $hostServiceSets = $directorHostObj->fetchServiceSets() + + AppliedServiceSetLoader::fetchForHost($directorHostObj); + + $parents = $templateResolver->fetchParents(); + + foreach ($parents as $parent) { + $hostServiceSets += $parent->fetchServiceSets(); + $hostServiceSets += AppliedServiceSetLoader::fetchForHost($parent); + } + + foreach ($hostServiceSets as $hostServiceSet) { + foreach ($hostServiceSet->getServiceObjects() as $setServiceObject) { + if ($setServiceObject->getObjectName() === $serviceName) { + $directorServiceObj = $setServiceObject; + + break 2; + } + } + } + } + + $i++; + } while (! $directorServiceObj); + } + } catch (NotFoundError $_) { + if ($service !== null) { + Logger::error("Failed to find service '%s' on host '%s'", $service->name, $host->name); + } else { + Logger::error('Failed to find host %s', $host->name); + } + + return false; + } + + if ($service === null) { + $fields = (new IcingaObjectFieldLoader($directorHostObj))->getFields(); + } else { + $fields = (new IcingaObjectFieldLoader($directorServiceObj))->getFields(); + } + + if (empty($fields)) { + return false; + } + + $fieldsWithDataLists = []; + foreach ($fields as $field) { + $this->fieldConfig[$field->get('varname')] = [ + 'label' => $field->get('caption'), + 'group' => $field->getCategoryName(), + 'visibility' => $field->getSetting('visibility') + ]; + + if ($field->get('datatype') === 'Icinga\Module\Director\DataType\DataTypeDatalist') { + $fieldsWithDataLists[$field->get('id')] = $field; + } elseif ($field->get('datatype') === 'Icinga\Module\Director\DataType\DataTypeDictionary') { + $this->dictionaryNames[] = $field->get('varname'); + } + } + + if (! empty($fieldsWithDataLists)) { + if ($this->db()->getDbType() === 'pgsql') { + $joinCondition = 'CAST(dds.setting_value AS INTEGER) = dde.list_id'; + } else { + $joinCondition = 'dds.setting_value = dde.list_id'; + } + + $dataListEntries = $db->select()->from( + ['dds' => 'director_datafield_setting'], + [ + 'dds.datafield_id', + 'dde.entry_name', + 'dde.entry_value' + ] + )->join( + ['dde' => 'director_datalist_entry'], + $joinCondition, + [] + )->where('dds.datafield_id', array_keys($fieldsWithDataLists)) + ->where('dds.setting_name', 'datalist_id'); + + foreach ($dataListEntries as $dataListEntry) { + $field = $fieldsWithDataLists[$dataListEntry->datafield_id]; + $this->datalistMaps[$field->get('varname')][$dataListEntry->entry_name] + = $dataListEntry->entry_value; + } + } + + return true; + } catch (Throwable $e) { + Logger::error("%s\n%s", $e, $e->getTraceAsString()); + + return false; + } + } + + public function renderCustomVarKey(string $key) + { + try { + if (isset($this->fieldConfig[$key]['label'])) { + return new HtmlElement( + 'span', + Attributes::create(['title' => $this->fieldConfig[$key]['label'] . " [$key]"]), + Text::create($this->fieldConfig[$key]['label']) + ); + } + } catch (Throwable $e) { + Logger::error("%s\n%s", $e, $e->getTraceAsString()); + } + + return null; + } + + public function renderCustomVarValue(string $key, $value) + { + try { + if (isset($this->fieldConfig[$key])) { + if ($this->fieldConfig[$key]['visibility'] === 'hidden') { + return '***'; + } + + if (isset($this->datalistMaps[$key][$value])) { + return new HtmlElement( + 'span', + Attributes::create(['title' => $this->datalistMaps[$key][$value] . " [$value]"]), + Text::create($this->datalistMaps[$key][$value]) + ); + } elseif ($value !== null && in_array($key, $this->dictionaryNames)) { + return $this->renderDictionaryVal($key, (array) $value); + } + } + } catch (Throwable $e) { + Logger::error("%s\n%s", $e, $e->getTraceAsString()); + } + + return null; + } + + public function identifyCustomVarGroup(string $key): ?string + { + if (isset($this->fieldConfig[$key]['group'])) { + return $this->fieldConfig[$key]['group']; + } + + return null; + } + + /** + * Render the dictionary value + * + * @param string $key + * @param array $value + * + * @return ?ValidHtml + */ + protected function renderDictionaryVal(string $key, array $value): ?ValidHtml + { + if ($this->dictionaryLevel > 0) { + $numItems = count($value); + $label = $this->renderCustomVarKey($key) ?? $key; + + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($label)), + new HtmlElement( + 'td', + null, + Text::create(sprintf(tp('%d item', '%d items', $numItems), $numItems)) + ) + ) + ); + } else { + $this->dictionaryTable = new HtmlElement( + 'table', + Attributes::create(['class' => ['custom-var-table', 'name-value-table']]) + ); + + $this->dictionaryBody = new HtmlElement('tbody'); + } + + $this->dictionaryLevel++; + + foreach ($value as $key => $val) { + if ($key !== null && is_array($val) || is_object($val)) { + $val = (array) $val; + $numChildItems = count($val); + + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($key)), + new HtmlElement( + 'td', + null, + Text::create(sprintf(tp('%d item', '%d items', $numChildItems), $numChildItems)) + ) + ) + ); + + $this->dictionaryLevel++; + foreach ($val as $childKey => $childVal) { + $childVal = $this->renderCustomVarValue($childKey, $childVal) ?? $childVal; + if (! in_array($childKey, $this->dictionaryNames)) { + $label = $this->renderCustomVarKey($childKey) ?? $childKey; + + if (is_array($childVal)) { + $this->renderArrayVal($label, $childVal); + } else { + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml( + $label + )), + new HtmlElement('td', null, Html::wantHtml($childVal)) + ) + ); + } + } + } + + $this->dictionaryLevel--; + } elseif (is_array($val)) { + $this->renderArrayVal($key, $val); + } else { + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($key)), + new HtmlElement('td', null, Html::wantHtml($val)) + ) + ); + } + } + + $this->dictionaryLevel--; + + if ($this->dictionaryLevel === 0) { + return $this->dictionaryTable->addHtml($this->dictionaryBody); + } + + return null; + } + + /** + * Render an array + * + * @param HtmlElement|string $name + * @param array $array + * + * @return void + */ + protected function renderArrayVal($name, array $array) + { + $numItems = count($array); + + if ($name instanceof HtmlElement) { + $name->addHtml(Text::create(' (Array)')); + } else { + $name = (new HtmlDocument())->addHtml( + Html::wantHtml($name), + Text::create(' (Array)') + ); + } + + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($name)), + new HtmlElement('td', null, Html::wantHtml(sprintf(tp('%d item', '%d items', $numItems), $numItems))) + ) + ); + + ++$this->dictionaryLevel; + + ksort($array); + foreach ($array as $key => $value) { + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml("[$key]")), + new HtmlElement('td', null, Html::wantHtml($value)) + ) + ); + } + + --$this->dictionaryLevel; + } +} diff --git a/library/Director/ProvidedHook/Monitoring/CustomVarRenderer.php b/library/Director/ProvidedHook/Monitoring/CustomVarRenderer.php new file mode 100644 index 000000000..4eb8d2d02 --- /dev/null +++ b/library/Director/ProvidedHook/Monitoring/CustomVarRenderer.php @@ -0,0 +1,429 @@ +get('db', 'resource'); + if (! $resourceName) { + throw new ConfigurationError('Cannot identify director db. No resource configured'); + } + + return Db::fromResourceName($resourceName); + } + + public function prefetchForObject(MonitoredObject $object) + { + try { + if ($object instanceof Host) { + $host = $object; + $service = null; + } elseif ($object instanceof Service) { + $host = $object->getHost(); + $service = $object; + } else { + return false; + } + + $db = $this->db(); + + try { + $directorHostObj = IcingaHost::load($host->getName(), $db); + if ($service !== null) { + $serviceOrigin = ['direct', 'inherited', 'applied', 'service-set']; + $serviceName = $service->getName(); + $i = 0; + $directorServiceObj = null; + do { + if ($i > 3) { + Logger::error("Failed to find service %s on host %s", $serviceName, $host->getName()); + + return false; + } elseif ($serviceOrigin[$i] === 'direct') { + $directorServiceObj = IcingaService::loadOptional([ + 'host_id' => $directorHostObj->get('id'), + 'object_name' => $serviceName + ], $db); + } elseif ($serviceOrigin[$i] == 'inherited') { + $templateResolver = new IcingaTemplateResolver($directorHostObj); + + $parentIds = $templateResolver->listParentIds(); + + $query = $db->getDbAdapter()->select()->from('icinga_service') + ->where('object_name = ?', $serviceName) + ->where('host_id IN (?)', $parentIds); + + $directorServices = IcingaService::loadAll( + $db, + $query, + 'object_name' + ); + + $directorServiceObj = current($directorServices); + } elseif ($serviceOrigin[$i] === 'applied') { + $appliedFilterQuery = IcingaHostAppliedServicesTable::load($directorHostObj)->getQuery(); + + foreach ($appliedFilterQuery->fetchAll() as $appliedService) { + if ($appliedService->name === $serviceName) { + $query = $db->getDbAdapter()->select()->from('icinga_service') + ->where('object_name = ?', $serviceName) + ->where("object_type = 'apply'") + ->where('assign_filter = ?', $appliedService->assign_filter); + + $directorAppliedServices = IcingaService::loadAll( + $db, + $query, + 'object_name' + ); + + $directorServiceObj = current($directorAppliedServices); + + break; + } + } + } elseif ($serviceOrigin[$i] === 'service-set') { + $templateResolver = new IcingaTemplateResolver($directorHostObj); + + $hostServiceSets = $directorHostObj->fetchServiceSets() + + AppliedServiceSetLoader::fetchForHost($directorHostObj); + + $parents = $templateResolver->fetchParents(); + + foreach ($parents as $parent) { + $hostServiceSets += $parent->fetchServiceSets(); + $hostServiceSets += AppliedServiceSetLoader::fetchForHost($parent); + } + + foreach ($hostServiceSets as $hostServiceSet) { + foreach ($hostServiceSet->getServiceObjects() as $setServiceObject) { + if ($setServiceObject->getObjectName() === $serviceName) { + $directorServiceObj = $setServiceObject; + + break 2; + } + } + } + } else { + return false; + } + + $i++; + } while (! $directorServiceObj); + } + } catch (NotFoundError $_) { + if ($service !== null) { + Logger::error("Failed to find service '%s' on host '%s'", $service->getName(), $host->getName()); + } else { + Logger::error("Failed to find host '%s'", $host->getName()); + } + + return false; + } + + if ($service === null) { + $fields = (new IcingaObjectFieldLoader($directorHostObj))->getFields(); + } else { + $fields = (new IcingaObjectFieldLoader($directorServiceObj))->getFields(); + } + + if (empty($fields)) { + return false; + } + + $fieldsWithDataLists = []; + foreach ($fields as $field) { + $this->fieldConfig[$field->get('varname')] = [ + 'label' => $field->get('caption'), + 'group' => $field->getCategoryName(), + 'visibility' => $field->getSetting('visibility') + ]; + + if ($field->get('datatype') === 'Icinga\Module\Director\DataType\DataTypeDatalist') { + $fieldsWithDataLists[$field->get('id')] = $field; + } elseif ($field->get('datatype') === 'Icinga\Module\Director\DataType\DataTypeDictionary') { + $this->dictionaryNames[] = $field->get('varname'); + } + } + + if (! empty($fieldsWithDataLists)) { + if ($this->db()->getDbType() === 'pgsql') { + $joinCondition = 'CAST(dds.setting_value AS INTEGER) = dde.list_id'; + } else { + $joinCondition = 'dds.setting_value = dde.list_id'; + } + + $dataListEntries = $db->select()->from( + ['dds' => 'director_datafield_setting'], + [ + 'dds.datafield_id', + 'dde.entry_name', + 'dde.entry_value' + ] + )->join( + ['dde' => 'director_datalist_entry'], + $joinCondition, + [] + )->where('dds.datafield_id', array_keys($fieldsWithDataLists)) + ->where('dds.setting_name', 'datalist_id'); + + foreach ($dataListEntries as $dataListEntry) { + $field = $fieldsWithDataLists[$dataListEntry->datafield_id]; + $this->datalistMaps[$field->get('varname')][$dataListEntry->entry_name] + = $dataListEntry->entry_value; + } + } + + return true; + } catch (Throwable $e) { + Logger::error("%s\n%s", $e, $e->getTraceAsString()); + + return false; + } + } + + public function renderCustomVarKey($key) + { + try { + if (isset($this->fieldConfig[$key]['label'])) { + return new HtmlElement( + 'span', + Attributes::create(['title' => $this->fieldConfig[$key]['label'] . " [$key]"]), + Text::create($this->fieldConfig[$key]['label']) + ); + } + } catch (Throwable $e) { + Logger::error("%s\n%s", $e, $e->getTraceAsString()); + } + } + + public function renderCustomVarValue($key, $value) + { + try { + if (isset($this->fieldConfig[$key])) { + if ($this->fieldConfig[$key]['visibility'] === 'hidden') { + return '***'; + } + + if (isset($this->datalistMaps[$key][$value])) { + return new HtmlElement( + 'span', + Attributes::create(['title' => $this->datalistMaps[$key][$value] . " [$value]"]), + Text::create($this->datalistMaps[$key][$value]) + ); + } elseif ($value !== null && in_array($key, $this->dictionaryNames)) { + return $this->renderDictionaryVal($key, (array) $value); + } + } + } catch (Throwable $e) { + Logger::error("%s\n%s", $e, $e->getTraceAsString()); + } + + return null; + } + + public function identifyCustomVarGroup($key) + { + if (isset($this->fieldConfig[$key]['group'])) { + return $this->fieldConfig[$key]['group']; + } + } + + /** + * Render the dictionary value + * + * @param string $key + * @param array $value + * + * @return ?ValidHtml + */ + protected function renderDictionaryVal(string $key, array $value): ?ValidHtml + { + if ($this->dictionaryLevel > 0) { + $numItems = count($value); + $label = $this->renderCustomVarKey($key) ?? $key; + + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($label)), + new HtmlElement( + 'td', + null, + Text::create(sprintf(tp('%d item', '%d items', $numItems), $numItems)) + ) + ) + ); + } else { + $this->dictionaryTable = new HtmlElement( + 'table', + Attributes::create(['class' => ['custom-var-table', 'name-value-table']]) + ); + + $this->dictionaryBody = new HtmlElement('tbody'); + } + + $this->dictionaryLevel++; + + foreach ($value as $key => $val) { + if ($key !== null && is_array($val) || is_object($val)) { + $val = (array) $val; + $numChildItems = count($val); + + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($key)), + new HtmlElement( + 'td', + null, + Text::create(sprintf(tp('%d item', '%d items', $numChildItems), $numChildItems)) + ) + ) + ); + + $this->dictionaryLevel++; + foreach ($val as $childKey => $childVal) { + $childVal = $this->renderCustomVarValue($childKey, $childVal) ?? $childVal; + if (! in_array($childKey, $this->dictionaryNames)) { + $label = $this->renderCustomVarKey($childKey) ?? $childKey; + + if (is_array($childVal)) { + $this->renderArrayVal($label, $childVal); + } else { + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml( + $label + )), + new HtmlElement('td', null, Html::wantHtml($childVal)) + ) + ); + } + } + } + + $this->dictionaryLevel--; + } elseif (is_array($val)) { + $this->renderArrayVal(Html::wantHtml($key), $val); + } else { + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($key)), + new HtmlElement('td', null, Html::wantHtml($val)) + ) + ); + } + } + + $this->dictionaryLevel--; + + if ($this->dictionaryLevel === 0) { + return $this->dictionaryTable->addHtml($this->dictionaryBody); + } + + return null; + } + + /** + * Render an array + * + * @param HtmlElement|string $name + * @param array $array + * + * @return void + */ + protected function renderArrayVal($name, array $array) + { + $numItems = count($array); + + if ($name instanceof HtmlElement) { + $name->addHtml(Text::create(' (Array)')); + } else { + $name = (new HtmlDocument())->addHtml( + Html::wantHtml($name), + Text::create(' (Array)') + ); + } + + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml($name)), + new HtmlElement('td', null, Html::wantHtml(sprintf(tp('%d item', '%d items', $numItems), $numItems))) + ) + ); + + ++$this->dictionaryLevel; + + ksort($array); + foreach ($array as $key => $value) { + $this->dictionaryBody->addHtml( + new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->dictionaryLevel}"]), + new HtmlElement('th', null, Html::wantHtml("[$key]")), + new HtmlElement('td', null, Html::wantHtml($value)) + ) + ); + } + + --$this->dictionaryLevel; + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 259e94060..ad8ee24af 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -240,11 +240,6 @@ parameters: count: 1 path: application/views/helpers/FormStoredPassword.php - - - message: "#^Binary operation \"\\*\\=\" between string and 1024 results in an error\\.$#" - count: 1 - path: library/Director/Application/MemoryLimit.php - - message: "#^Constructor of class Icinga\\\\Module\\\\Director\\\\Core\\\\RestApiClient has an unused parameter \\$cn\\.$#" count: 1 @@ -395,21 +390,6 @@ parameters: count: 1 path: library/Director/IcingaConfig/AgentWizard.php - - - message: "#^Binary operation \"\\*\" between string and 3600 results in an error\\.$#" - count: 1 - path: library/Director/IcingaConfig/IcingaConfigHelper.php - - - - message: "#^Binary operation \"\\*\" between string and 60 results in an error\\.$#" - count: 1 - path: library/Director/IcingaConfig/IcingaConfigHelper.php - - - - message: "#^Binary operation \"\\*\" between string and 86400 results in an error\\.$#" - count: 1 - path: library/Director/IcingaConfig/IcingaConfigHelper.php - - message: "#^Call to an undefined method Icinga\\\\Module\\\\Director\\\\Web\\\\Form\\\\QuickForm\\:\\:getSentOrObjectSetting\\(\\)\\.$#" count: 3 diff --git a/register-hooks.php b/register-hooks.php index 36e7e7018..5c72f321e 100644 --- a/register-hooks.php +++ b/register-hooks.php @@ -71,6 +71,8 @@ $this->provideHook('icingadb/icingadbSupport'); $this->provideHook('cube/Actions', CubeLinks::class); $this->provideHook('cube/IcingaDbActions', IcingaDbCubeLinks::class); + $this->provideHook('Icingadb/CustomVarRenderer'); + $this->provideHook('Monitoring/CustomVarRenderer'); } $directorHooks = [