icingadb-web/library/Icingadb/Common/ObjectInspectionDetail.php
Alexander A. Klimov 3c8ed68cc6 Upgrade license from GPLv2 to GPLv2+
This was easy because only README.md and doc/01-About.md were redacted manually, everything else via:
git ls-files -z |xargs -0 perl -pi -e 's/Icinga GmbH \| GPLv2/Icinga GmbH | GPLv2+/'

This is legal because we have only merged PRs with label:cla/signed or made by Icinga staff:
https://github.com/Icinga/icingadb-web/pulls?page=1&q=is%3Apr+is%3Aclosed+-label%3Acla%2Fsigned+-author%3Anilmerg

This has no risk for us in people distributing their own version under GPLv3 only.
After all, we won't take their patches anyway, unless they sign our CLA.

This is the cleanest solution for having e.g. these in one address space:

* Icinga Web, GPLv2+
* K8s Web, AGPLv3
* Thirdparty, some LGPLv3 and Apache-2.0

Apropos, K8s Web is even v3-licensed on purpose, to have a stronger protection against cloud ops.
2025-11-21 13:31:24 +01:00

347 lines
11 KiB
PHP

<?php
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Module\Icingadb\Common;
use DateTime;
use DateTimeZone;
use Exception;
use Icinga\Exception\IcingaException;
use Icinga\Exception\Json\JsonDecodeException;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use Icinga\Module\Icingadb\Widget\Detail\CustomVarTable;
use Icinga\Util\Format;
use Icinga\Util\Json;
use InvalidArgumentException;
use ipl\Html\BaseHtmlElement;
use ipl\Html\FormattedString;
use ipl\Html\HtmlElement;
use ipl\Html\Table;
use ipl\Html\Text;
use ipl\Orm\Model;
use ipl\Web\Widget\CopyToClipboard;
use ipl\Web\Widget\EmptyState;
abstract class ObjectInspectionDetail extends BaseHtmlElement
{
protected $tag = 'div';
protected $defaultAttributes = ['class' => ['object-detail', 'inspection-detail']];
/** @var Model */
protected $object;
/** @var array */
protected $attrs;
/** @var array */
protected $joins;
public function __construct(Model $object, array $apiResult)
{
$this->object = $object;
$this->attrs = $apiResult['attrs'];
$this->joins = $apiResult['joins'];
}
/**
* Render the object source location
*
* @return ?array
*/
protected function createSourceLocation()
{
if (! isset($this->attrs['source_location'])) {
return;
}
return [
new HtmlElement('h2', null, Text::create(t('Source Location'))),
FormattedString::create(
t('You can find this object in %s on line %s.'),
new HtmlElement('strong', null, Text::create($this->attrs['source_location']['path'])),
new HtmlElement('strong', null, Text::create($this->attrs['source_location']['first_line']))
)
];
}
/**
* Render object's last check result
*
* @return ?array
*/
protected function createLastCheckResult()
{
if (! isset($this->attrs['last_check_result'])) {
return;
}
$command = $this->attrs['last_check_result']['command'];
if (is_array($command)) {
$command = join(' ', array_map('escapeshellarg', $command));
}
$denylist = [
'command',
'output',
'type',
'active'
];
if ($command) {
$execCommand = new HtmlElement('pre', null, Text::create($command));
CopyToClipboard::attachTo($execCommand);
} else {
$execCommand = new EmptyState(t('n. a.'));
}
return [
new HtmlElement('h2', null, Text::create(t('Executed Command'))),
$execCommand,
new HtmlElement('h2', null, Text::create(t('Execution Details'))),
$this->createNameValueTable(
array_diff_key($this->attrs['last_check_result'], array_flip($denylist)),
[
'execution_end' => [$this, 'formatTimestamp'],
'execution_start' => [$this, 'formatTimestamp'],
'schedule_end' => [$this, 'formatTimestamp'],
'schedule_start' => [$this, 'formatTimestamp'],
'ttl' => [$this, 'formatSeconds'],
'state' => [$this, 'formatState']
]
)
];
}
protected function createRedisInfo(): array
{
$title = new HtmlElement('h2', null, Text::create(t('Volatile State Details')));
try {
$json = Backend::getRedis()->getConnection()
->hGet("icinga:{$this->object->getTableName()}:state", bin2hex($this->object->id));
} catch (Exception $e) {
return [$title, sprintf('Failed to load redis data: %s', $e->getMessage())];
}
if (! $json) {
return [$title, new EmptyState(t('No data available in redis'))];
}
try {
$data = Json::decode($json, true);
} catch (JsonDecodeException $e) {
return [$title, sprintf('Failed to decode redis data: %s', $e->getMessage())];
}
$denylist = [
'commandline',
'environment_id',
'id'
];
return [$title, $this->createNameValueTable(
array_diff_key($data, array_flip($denylist)),
[
'last_state_change' => [$this, 'formatMillisecondTimestamp'],
'last_update' => [$this, 'formatMillisecondTimestamp'],
'next_check' => [$this, 'formatMillisecondTimestamp'],
'next_update' => [$this, 'formatMillisecondTimestamp'],
'check_timeout' => [$this, 'formatMilliseconds'],
'execution_time' => [$this, 'formatMilliseconds'],
'latency' => [$this, 'formatMilliseconds'],
'hard_state' => [$this, 'formatState'],
'previous_soft_state' => [$this, 'formatState'],
'previous_hard_state' => [$this, 'formatState'],
'state' => [$this, 'formatState']
]
)];
}
protected function createAttributes(): array
{
$denylist = [
'name',
'__name',
'host_name',
'display_name',
'last_check_result',
'source_location',
'package',
'version',
'type',
'active',
'paused',
'ha_mode'
];
return [
new HtmlElement('h2', null, Text::create(t('Object Attributes'))),
$this->createNameValueTable(
array_diff_key($this->attrs, array_flip($denylist)),
[
'acknowledgement_expiry' => [$this, 'formatTimestamp'],
'acknowledgement_last_change' => [$this, 'formatTimestamp'],
'check_timeout' => [$this, 'formatSeconds'],
'flapping_last_change' => [$this, 'formatTimestamp'],
'last_check' => [$this, 'formatTimestamp'],
'last_hard_state_change' => [$this, 'formatTimestamp'],
'last_state_change' => [$this, 'formatTimestamp'],
'last_state_ok' => [$this, 'formatTimestamp'],
'last_state_up' => [$this, 'formatTimestamp'],
'last_state_warning' => [$this, 'formatTimestamp'],
'last_state_critical' => [$this, 'formatTimestamp'],
'last_state_down' => [$this, 'formatTimestamp'],
'last_state_unknown' => [$this, 'formatTimestamp'],
'last_state_unreachable' => [$this, 'formatTimestamp'],
'next_check' => [$this, 'formatTimestamp'],
'next_update' => [$this, 'formatTimestamp'],
'previous_state_change' => [$this, 'formatTimestamp'],
'check_interval' => [$this, 'formatSeconds'],
'retry_interval' => [$this, 'formatSeconds'],
'last_hard_state' => [$this, 'formatState'],
'last_state' => [$this, 'formatState'],
'state' => [$this, 'formatState']
]
)
];
}
protected function createCustomVariables()
{
$query = $this->object->customvar
->columns(['name', 'value']);
$result = [];
foreach ($query as $row) {
$result[$row->name] = json_decode($row->value, true) ?? $row->value;
}
if (! empty($result)) {
$vars = new CustomVarTable($result);
} else {
$vars = new EmptyState(t('No custom variables configured.'));
}
return [
new HtmlElement('h2', null, Text::create(t('Custom Variables'))),
$vars
];
}
/**
* Format the given value as a json
*
* @param mixed $json
*
* @return BaseHtmlElement|string
*/
private function formatJson($json)
{
if (is_scalar($json)) {
return Json::encode($json, JSON_UNESCAPED_SLASHES);
}
return new HtmlElement(
'pre',
null,
Text::create(Json::encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))
);
}
/**
* Format the given timestamp
*
* @param int|float|null $ts
*
* @return EmptyState|string
*/
private function formatTimestamp($ts)
{
if (empty($ts)) {
return new EmptyState(t('n. a.'));
}
if (is_float($ts)) {
$dt = DateTime::createFromFormat('U.u', sprintf('%F', $ts));
} else {
$dt = (new DateTime())->setTimestamp($ts);
}
return $dt->setTimezone(new DateTimeZone('UTC'))
->format('Y-m-d\TH:i:s.vP');
}
/**
* Format the given timestamp (in milliseconds)
*
* @param int|float|null $ms
*
* @return EmptyState|string
*/
private function formatMillisecondTimestamp($ms)
{
return $this->formatTimestamp($ms / 1000.0);
}
private function formatSeconds($s): string
{
return Format::seconds($s);
}
private function formatMilliseconds($ms): string
{
return Format::seconds($ms / 1000.0);
}
private function formatState(int $state)
{
try {
switch (true) {
case $this->object instanceof Host:
return HostStates::text($state);
case $this->object instanceof Service:
return ServiceStates::text($state);
default:
return $state;
}
} catch (InvalidArgumentException $_) {
// The Icinga 2 API sometimes delivers strange details
return (string) $state;
}
}
private function createNameValueTable(array $data, array $formatters): Table
{
$table = new Table();
$table->addAttributes(['class' => 'name-value-table']);
foreach ($data as $name => $value) {
if (empty($value) && ($value === null || is_string($value) || is_array($value))) {
$value = new EmptyState(t('n. a.'));
} else {
try {
if (isset($formatters[$name])) {
$value = call_user_func($formatters[$name], $value);
} else {
$value = $this->formatJson($value);
if ($value instanceof BaseHtmlElement) {
CopyToClipboard::attachTo($value);
}
}
} catch (Exception $e) {
$value = new EmptyState(IcingaException::describe($e));
}
}
$table->addHtml(Table::tr([
Table::th($name),
Table::td($value)
]));
}
return $table;
}
}