diff options
Diffstat (limited to 'library/Director/Web/Widget')
22 files changed, 2515 insertions, 0 deletions
diff --git a/library/Director/Web/Widget/AbstractList.php b/library/Director/Web/Widget/AbstractList.php new file mode 100644 index 0000000..ad1b9e3 --- /dev/null +++ b/library/Director/Web/Widget/AbstractList.php @@ -0,0 +1,40 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlElement; + +class AbstractList extends BaseHtmlElement +{ + protected $contentSeparator = "\n"; + + /** + * AbstractList constructor. + * @param array $items + * @param null $attributes + */ + public function __construct(array $items = [], $attributes = null) + { + foreach ($items as $item) { + $this->addItem($item); + } + + if ($attributes !== null) { + $this->addAttributes($attributes); + } + } + + /** + * @param Html|array|string $content + * @param Attributes|array $attributes + * + * @return $this + */ + public function addItem($content, $attributes = null) + { + return $this->add(HtmlElement::create('li', $attributes, $content)); + } +} diff --git a/library/Director/Web/Widget/ActivityLogInfo.php b/library/Director/Web/Widget/ActivityLogInfo.php new file mode 100644 index 0000000..8454b26 --- /dev/null +++ b/library/Director/Web/Widget/ActivityLogInfo.php @@ -0,0 +1,634 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Json\JsonString; +use Icinga\Module\Director\Objects\DirectorActivityLog; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use Icinga\Date\DateFormatter; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Forms\RestoreObjectForm; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Url; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use gipfl\IcingaWeb2\Widget\Tabs; + +class ActivityLogInfo extends HtmlDocument +{ + use TranslationHelper; + + protected $defaultTab; + + /** @var Db */ + protected $db; + + /** @var string */ + protected $type; + + /** @var string */ + protected $typeName; + + /** @var string */ + protected $name; + + protected $entry; + + protected $oldProperties; + + protected $newProperties; + + protected $oldObject; + + /** @var Tabs */ + protected $tabs; + + /** @var int */ + protected $id; + + public function __construct(Db $db, $type = null, $name = null) + { + $this->db = $db; + if ($type !== null) { + $this->setType($type); + } + $this->name = $name; + } + + public function setType($type) + { + $this->type = $type; + $this->typeName = $this->translate( + ucfirst(preg_replace('/^icinga_/', '', $type)) // really? + ); + + return $this; + } + + /** + * @param Url $url + * @return HtmlElement + * @throws \Icinga\Exception\IcingaException + */ + public function getPagination(Url $url) + { + /** @var Url $url */ + $url = $url->without('checksum')->without('show'); + $div = Html::tag('div', [ + 'class' => 'pagination-control', + 'style' => 'float: right; width: 5em' + ]); + + $ul = Html::tag('ul', ['class' => 'nav tab-nav']); + $li = Html::tag('li', ['class' => 'nav-item']); + $ul->add($li); + $neighbors = $this->getNeighbors(); + $iconLeft = new Icon('angle-double-left'); + $iconRight = new Icon('angle-double-right'); + if ($neighbors->prev) { + $li->add(new Link($iconLeft, $url->with('id', $neighbors->prev))); + } else { + $li->add(Html::tag('span', ['class' => 'disabled'], $iconLeft)); + } + + $li = Html::tag('li', ['class' => 'nav-item']); + $ul->add($li); + if ($neighbors->next) { + $li->add(new Link($iconRight, $url->with('id', $neighbors->next))); + } else { + $li->add(Html::tag('span', ['class' => 'disabled'], $iconRight)); + } + + return $div->add($ul); + } + + /** + * @param $tabName + * @return $this + * @throws \Icinga\Exception\Http\HttpNotFoundException + * @throws \Icinga\Exception\IcingaException + */ + public function showTab($tabName) + { + if ($tabName === null) { + $tabName = $this->defaultTab; + } + + $this->getTabs()->activate($tabName); + $this->add($this->getInfoTable()); + if ($tabName === 'old') { + // $title = sprintf('%s former config', $this->entry->object_name); + $diffs = IcingaConfigDiff::getDiffs($this->oldConfig(), $this->emptyConfig()); + } elseif ($tabName === 'new') { + // $title = sprintf('%s new config', $this->entry->object_name); + $diffs = IcingaConfigDiff::getDiffs($this->emptyConfig(), $this->newConfig()); + } else { + $diffs = IcingaConfigDiff::getDiffs($this->oldConfig(), $this->newConfig()); + } + + $this->addDiffs($diffs); + + return $this; + } + + protected function emptyConfig() + { + return new IcingaConfig($this->db); + } + + /** + * @param $diffs + * @throws \Icinga\Exception\IcingaException + */ + protected function addDiffs($diffs) + { + foreach ($diffs as $file => $diff) { + $this->add(Html::tag('h3', null, $file))->add($diff); + } + } + + /** + * @return RestoreObjectForm + * @throws \Icinga\Exception\IcingaException + */ + protected function getRestoreForm() + { + return RestoreObjectForm::load() + ->setDb($this->db) + ->setObject($this->oldObject()) + ->handleRequest(); + } + + public function setChecksum($checksum) + { + if ($checksum !== null) { + $this->entry = $this->db->fetchActivityLogEntry($checksum); + $this->id = (int) $this->entry->id; + } + + return $this; + } + + public function setId($id) + { + if ($id !== null) { + $this->entry = $this->db->fetchActivityLogEntryById($id); + $this->id = (int) $id; + } + + return $this; + } + + public function getNeighbors() + { + return $this->db->getActivitylogNeighbors( + $this->id, + $this->type, + $this->name + ); + } + + public function getCurrentObject() + { + return IcingaObject::loadByType( + $this->type, + $this->name, + $this->db + ); + } + + /** + * @return bool + * @deprecated No longer used? + */ + public function objectStillExists() + { + return IcingaObject::existsByType( + $this->type, + $this->objectKey(), + $this->db + ); + } + + protected function oldProperties() + { + if ($this->oldProperties === null) { + if (property_exists($this->entry, 'old_properties')) { + $this->oldProperties = JsonString::decodeOptional($this->entry->old_properties); + } + if ($this->oldProperties === null) { + $this->oldProperties = new \stdClass; + } + } + + return $this->oldProperties; + } + + protected function newProperties() + { + if ($this->newProperties === null) { + if (property_exists($this->entry, 'new_properties')) { + $this->newProperties = JsonString::decodeOptional($this->entry->new_properties); + } + if ($this->newProperties === null) { + $this->newProperties = new \stdClass; + } + } + + return $this->newProperties; + } + + protected function getEntryProperty($key) + { + $entry = $this->entry; + + if (property_exists($entry, $key)) { + return $entry->{$key}; + } elseif (property_exists($this->newProperties(), $key)) { + return $this->newProperties->{$key}; + } elseif (property_exists($this->oldProperties(), $key)) { + return $this->oldProperties->{$key}; + } else { + return null; + } + } + + protected function objectLinkParams() + { + $entry = $this->entry; + + $params = ['name' => $entry->object_name]; + + if ($entry->object_type === 'icinga_service') { + if (($set = $this->getEntryProperty('service_set')) !== null) { + $params['set'] = $set; + return $params; + } elseif (($host = $this->getEntryProperty('host')) !== null) { + $params['host'] = $host; + return $params; + } else { + return $params; + } + } elseif ($entry->object_type === 'icinga_service_set') { + return $params; + } else { + return $params; + } + } + + protected function getActionExtraHtml() + { + $entry = $this->entry; + + $info = ''; + $host = null; + + if ($entry->object_type === 'icinga_service') { + if (($set = $this->getEntryProperty('service_set')) !== null) { + $info = Html::sprintf( + '%s "%s"', + $this->translate('on service set'), + Link::create( + $set, + 'director/serviceset', + ['name' => $set], + ['data-base-target' => '_next'] + ) + ); + } else { + $host = $this->getEntryProperty('host'); + } + } elseif ($entry->object_type === 'icinga_service_set') { + $host = $this->getEntryProperty('host'); + } + + if ($host !== null) { + $info = Html::sprintf( + '%s "%s"', + $this->translate('on host'), + Link::create( + $host, + 'director/host', + ['name' => $host], + ['data-base-target' => '_next'] + ) + ); + } + + return $info; + } + + /** + * @return array + * @deprecated No longer used? + */ + protected function objectKey() + { + $entry = $this->entry; + if ($entry->object_type === 'icinga_service' || $entry->object_type === 'icinga_service_set') { + // TODO: this is not correct. Activity needs to get (multi) key support + return ['name' => $entry->object_name]; + } + + return $entry->object_name; + } + + /** + * @param Url|null $url + * @return Tabs + */ + public function getTabs(Url $url = null) + { + if ($this->tabs === null) { + $this->tabs = $this->createTabs($url); + } + + return $this->tabs; + } + + /** + * @param Url $url + * @return Tabs + */ + public function createTabs(Url $url) + { + $entry = $this->entry; + $tabs = new Tabs(); + if ($entry->action_name === DirectorActivityLog::ACTION_MODIFY) { + $tabs->add('diff', [ + 'label' => $this->translate('Diff'), + 'url' => $url->without('show')->with('id', $entry->id) + ]); + + $this->defaultTab = 'diff'; + } + + if (in_array($entry->action_name, [ + DirectorActivityLog::ACTION_CREATE, + DirectorActivityLog::ACTION_MODIFY, + ])) { + $tabs->add('new', [ + 'label' => $this->translate('New object'), + 'url' => $url->with(['id' => $entry->id, 'show' => 'new']) + ]); + + if ($this->defaultTab === null) { + $this->defaultTab = 'new'; + } + } + + if (in_array($entry->action_name, [ + DirectorActivityLog::ACTION_DELETE, + DirectorActivityLog::ACTION_MODIFY, + ])) { + $tabs->add('old', [ + 'label' => $this->translate('Former object'), + 'url' => $url->with(['id' => $entry->id, 'show' => 'old']) + ]); + + if ($this->defaultTab === null) { + $this->defaultTab = 'old'; + } + } + + return $tabs; + } + + /** + * @return IcingaObject + * @throws \Icinga\Exception\IcingaException + */ + protected function oldObject() + { + if ($this->oldObject === null) { + $this->oldObject = $this->createObject( + $this->entry->object_type, + $this->entry->old_properties + ); + } + + return $this->oldObject; + } + + /** + * @return IcingaObject + * @throws \Icinga\Exception\IcingaException + */ + protected function newObject() + { + return $this->createObject( + $this->entry->object_type, + $this->entry->new_properties + ); + } + + protected function objectToConfig(IcingaObject $object) + { + if ($object instanceof IcingaService) { + return $this->previewService($object); + } else { + return $object->toSingleIcingaConfig(); + } + } + + protected function previewService(IcingaService $service) + { + if (($set = $service->get('service_set')) !== null) { + // simulate rendering of service in set + $set = IcingaServiceSet::load($set, $this->db); + + $service->set('service_set_id', null); + if (($assign = $set->get('assign_filter')) !== null) { + $service->set('object_type', 'apply'); + $service->set('assign_filter', $assign); + } + } + + return $service->toSingleIcingaConfig(); + } + + /** + * @return IcingaConfig + * @throws \Icinga\Exception\IcingaException + */ + protected function newConfig() + { + return $this->objectToConfig($this->newObject()); + } + + /** + * @return IcingaConfig + * @throws \Icinga\Exception\IcingaException + */ + protected function oldConfig() + { + return $this->objectToConfig($this->oldObject()); + } + + protected function getLinkToObject() + { + // TODO: This logic is redundant and should be centralized + $entry = $this->entry; + $name = $entry->object_name; + $controller = preg_replace('/^icinga_/', '', $entry->object_type); + + if ($controller === 'service_set') { + $controller = 'serviceset'; + } elseif ($controller === 'scheduled_downtime') { + $controller = 'scheduled-downtime'; + } + + return Link::create( + $name, + 'director/' . $controller, + $this->objectLinkParams(), + ['data-base-target' => '_next'] + ); + } + + /** + * @return NameValueTable + * @throws \Icinga\Exception\IcingaException + */ + public function getInfoTable() + { + $entry = $this->entry; + $table = new NameValueTable(); + $table->addNameValuePairs([ + $this->translate('Author') => $entry->author, + $this->translate('Date') => DateFormatter::formatDateTime( + $entry->change_time_ts + ), + + ]); + if (null === $this->name) { + $table->addNameValueRow( + $this->translate('Action'), + Html::sprintf( + '%s %s "%s" %s', + $entry->action_name, + $entry->object_type, + $this->getLinkToObject(), + $this->getActionExtraHtml() + ) + ); + } else { + $table->addNameValueRow( + $this->translate('Action'), + $entry->action_name + ); + } + + if ($comment = $this->getOptionalRangeComment()) { + $table->addNameValueRow( + $this->translate('Remark'), + $comment + ); + } + + if ($this->hasBeenEnabled()) { + $table->addNameValueRow( + $this->translate('Rendering'), + $this->translate('This object has been enabled') + ); + } elseif ($this->hasBeenDisabled()) { + $table->addNameValueRow( + $this->translate('Rendering'), + $this->translate('This object has been disabled') + ); + } + + $table->addNameValueRow( + $this->translate('Checksum'), + $entry->checksum + ); + if ($this->entry->old_properties) { + $table->addNameValueRow( + $this->translate('Actions'), + $this->getRestoreForm() + ); + } + + return $table; + } + + public function hasBeenEnabled() + { + return false; + } + + public function hasBeenDisabled() + { + return false; + } + + /** + * @return string + * @throws ProgrammingError + */ + public function getTitle() + { + switch ($this->entry->action_name) { + case DirectorActivityLog::ACTION_CREATE: + $msg = $this->translate('%s "%s" has been created'); + break; + case DirectorActivityLog::ACTION_DELETE: + $msg = $this->translate('%s "%s" has been deleted'); + break; + case DirectorActivityLog::ACTION_MODIFY: + $msg = $this->translate('%s "%s" has been modified'); + break; + default: + throw new ProgrammingError( + 'Unable to deal with "%s" activity', + $this->entry->action_name + ); + } + + return sprintf($msg, $this->typeName, $this->entry->object_name); + } + + protected function getOptionalRangeComment() + { + if ($this->id) { + $db = $this->db->getDbAdapter(); + return $db->fetchOne( + $db->select() + ->from('director_activity_log_remark', 'remark') + ->where('first_related_activity <= ?', $this->id) + ->where('last_related_activity >= ?', $this->id) + ); + } + + return null; + } + + /** + * @param $type + * @param $props + * @return IcingaObject + * @throws \Icinga\Exception\IcingaException + */ + protected function createObject($type, $props) + { + $props = json_decode($props); + $newProps = ['object_name' => $props->object_name]; + if (property_exists($props, 'object_type')) { + $newProps['object_type'] = $props->object_type; + } + + return IcingaObject::createByType( + $type, + $newProps, + $this->db + )->setProperties((array) $props); + } +} diff --git a/library/Director/Web/Widget/AdditionalTableActions.php b/library/Director/Web/Widget/AdditionalTableActions.php new file mode 100644 index 0000000..978f399 --- /dev/null +++ b/library/Director/Web/Widget/AdditionalTableActions.php @@ -0,0 +1,158 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Web\Table\FilterableByUsage; + +class AdditionalTableActions +{ + use TranslationHelper; + + /** @var Auth */ + protected $auth; + + /** @var Url */ + protected $url; + + /** @var ZfQueryBasedTable */ + protected $table; + + public function __construct(Auth $auth, Url $url, ZfQueryBasedTable $table) + { + $this->auth = $auth; + $this->url = $url; + $this->table = $table; + } + + public function appendTo(HtmlDocument $parent) + { + $links = []; + if ($this->hasPermission('director/admin')) { + $links[] = $this->createDownloadJsonLink(); + } + if ($this->hasPermission('director/showsql')) { + $links[] = $this->createShowSqlToggle(); + } + + if ($this->table instanceof FilterableByUsage) { + $parent->add($this->showUsageFilter($this->table)); + } + + if (! empty($links)) { + $parent->add($this->moreOptions($links)); + } + + return $this; + } + + protected function createDownloadJsonLink() + { + return Link::create( + $this->translate('Download as JSON'), + $this->url->with('format', 'json'), + null, + ['target' => '_blank'] + ); + } + + protected function createShowSqlToggle() + { + if ($this->url->getParam('format') === 'sql') { + $link = Link::create( + $this->translate('Hide SQL'), + $this->url->without('format') + ); + } else { + $link = Link::create( + $this->translate('Show SQL'), + $this->url->with('format', 'sql') + ); + } + + return $link; + } + + protected function showUsageFilter(FilterableByUsage $table) + { + $active = $this->url->getParam('usage', 'all'); + $links = [ + Link::create($this->translate('all'), $this->url->without('usage')), + Link::create($this->translate('used'), $this->url->with('usage', 'used')), + Link::create($this->translate('unused'), $this->url->with('usage', 'unused')), + ]; + + if ($active === 'used') { + $table->showOnlyUsed(); + } elseif ($active === 'unused') { + $table->showOnlyUnUsed(); + } + + $options = $this->ul( + $this->li([ + Link::create( + sprintf($this->translate('Usage (%s)'), $active), + '#', + null, + [ + 'class' => 'icon-sitemap' + ] + ), + $subUl = Html::tag('ul') + ]), + ['class' => 'nav'] + ); + + foreach ($links as $link) { + $subUl->add($this->li($link)); + } + + return $options; + } + + protected function moreOptions($links) + { + $options = $this->ul( + $this->li([ + // TODO: extend link for dropdown-toggle from Web 2, doesn't + // seem to work: [..], null, ['class' => 'dropdown-toggle'] + Link::create(Icon::create('down-open'), '#'), + $subUl = Html::tag('ul') + ]), + ['class' => 'nav'] + ); + + foreach ($links as $link) { + $subUl->add($this->li($link)); + } + + return $options; + } + + protected function ulLi($content) + { + return $this->ul($this->li($content)); + } + + protected function ul($content, $attributes = null) + { + return Html::tag('ul', $attributes, $content); + } + + protected function li($content) + { + return Html::tag('li', null, $content); + } + + protected function hasPermission($permission) + { + return $this->auth->hasPermission($permission); + } +} diff --git a/library/Director/Web/Widget/BackgroundDaemonDetails.php b/library/Director/Web/Widget/BackgroundDaemonDetails.php new file mode 100644 index 0000000..b4c33dd --- /dev/null +++ b/library/Director/Web/Widget/BackgroundDaemonDetails.php @@ -0,0 +1,131 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use gipfl\Translation\TranslationHelper; +use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Daemon\RunningDaemonInfo; +use Icinga\Util\Format; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\Table; + +class BackgroundDaemonDetails extends BaseHtmlElement +{ + use TranslationHelper; + + protected $tag = 'div'; + + /** @var RunningDaemonInfo */ + protected $info; + + /** @var \stdClass TODO: get rid of this */ + protected $daemon; + + public function __construct(RunningDaemonInfo $info, $daemon) + { + $this->info = $info; + $this->daemon = $daemon; + } + + protected function assemble() + { + $info = $this->info; + if ($info->hasBeenStopped()) { + $this->add(Hint::error(Html::sprintf( + $this->translate( + 'Daemon has been stopped %s, was running with PID %s as %s@%s' + ), + // $info->getHexUuid(), + $this->timeAgo($info->getTimestampStopped() / 1000), + Html::tag('strong', (string) $info->getPid()), + Html::tag('strong', $info->getUsername()), + Html::tag('strong', $info->getFqdn()) + ))); + } elseif ($info->isOutdated()) { + $this->add(Hint::error(Html::sprintf( + $this->translate( + 'Daemon keep-alive is outdated, was last seen running with PID %s as %s@%s %s' + ), + // $info->getHexUuid(), + Html::tag('strong', (string) $info->getPid()), + Html::tag('strong', $info->getUsername()), + Html::tag('strong', $info->getFqdn()), + $this->timeAgo($info->getLastUpdate() / 1000) + ))); + } else { + $this->add(Hint::ok(Html::sprintf( + $this->translate( + 'Daemon is running with PID %s as %s@%s, last refresh happened %s' + ), + // $info->getHexUuid(), + Html::tag('strong', (string)$info->getPid()), + Html::tag('strong', $info->getUsername()), + Html::tag('strong', $info->getFqdn()), + $this->timeAgo($info->getLastUpdate() / 1000) + ))); + $details = new NameValueTable(); + $details->addNameValuePairs([ + $this->translate('Startup Time') => DateFormatter::formatDateTime($info->getTimestampStarted() / 1000), + $this->translate('PID') => $info->getPid(), + $this->translate('Username') => $info->getUsername(), + $this->translate('FQDN') => $info->getFqdn(), + $this->translate('Running with systemd') => $info->isRunningWithSystemd() + ? $this->translate('yes') + : $this->translate('no'), + $this->translate('Binary') => $info->getBinaryPath() + . ($info->binaryRealpathDiffers() ? ' -> ' . $info->getBinaryRealpath() : ''), + $this->translate('PHP Binary') => $info->getPhpBinaryPath() + . ($info->phpBinaryRealpathDiffers() ? ' -> ' . $info->getPhpBinaryRealpath() : ''), + $this->translate('PHP Version') => $info->getPhpVersion(), + $this->translate('PHP Integer') => $info->has64bitIntegers() + ? '64bit' + : Html::sprintf( + '%sbit (%s)', + $info->getPhpIntegerSize() * 8, + Html::tag('span', ['class' => 'error'], $this->translate('unsupported')) + ), + ]); + $this->add($details); + + $this->add(Html::tag('h2', $this->translate('Process List'))); + if (\is_string($this->daemon->process_info)) { + // from DB: + $processes = \json_decode($this->daemon->process_info); + } else { + // via RPC: + $processes = $this->daemon->process_info; + } + $table = new Table(); + $table->add(Html::tag('thead', Html::tag('tr', Html::wrapEach([ + 'PID', + 'Command', + 'Memory' + ], 'th')))); + $table->setAttribute('class', 'common-table'); + foreach ($processes as $pid => $process) { + $table->add($table::row([ + [ + Icon::create($process->running ? 'ok' : 'warning-empty'), + ' ', + $pid + ], + Html::tag('pre', $process->command), + $process->memory === false ? 'n/a' : Format::bytes($process->memory->rss) + ])); + } + $this->add($table); + } + } + + protected function timeAgo($time) + { + return Html::tag('span', [ + 'class' => 'time-ago', + 'title' => DateFormatter::formatDateTime($time) + ], DateFormatter::timeAgo($time)); + } +} diff --git a/library/Director/Web/Widget/BranchedObjectHint.php b/library/Director/Web/Widget/BranchedObjectHint.php new file mode 100644 index 0000000..ec16094 --- /dev/null +++ b/library/Director/Web/Widget/BranchedObjectHint.php @@ -0,0 +1,69 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Translation\TranslationHelper; +use gipfl\Web\Widget\Hint; +use Icinga\Authentication\Auth; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchedObject; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class BranchedObjectHint extends HtmlDocument +{ + use TranslationHelper; + + public function __construct(Branch $branch, Auth $auth, BranchedObject $object = null) + { + if (! $branch->isBranch()) { + return; + } + $hook = Branch::requireHook(); + + $name = $branch->getName(); + if (substr($name, 0, 1) === '/') { + $label = $this->translate('this configuration branch'); + } else { + $label = $name; + } + $link = $hook->linkToBranch($branch, $auth, $label); + if ($object === null) { + $this->add(Hint::info(Html::sprintf($this->translate( + 'This object will be created in %s. It will not be part of any deployment' + . ' unless being merged' + ), $link))); + return; + } + + if (! $object->hasBeenTouchedByBranch()) { + $this->add(Hint::info(Html::sprintf($this->translate( + 'Your changes will be stored in %s. The\'ll not be part of any deployment' + . ' unless being merged' + ), $link))); + return; + } + + if ($object->hasBeenDeletedByBranch()) { + throw new NotFoundError('No such object available'); + // Alternative, requires hiding other actions: + // $this->add(Hint::info(Html::sprintf( + // $this->translate('This object has been deleted in %s'), + // $link + // ))); + } elseif ($object->hasBeenCreatedByBranch()) { + $this->add(Hint::info(Html::sprintf( + $this->translate('This object has been created in %s'), + $link + ))); + } else { + $this->add(Hint::info(Html::sprintf( + $this->translate('This object has modifications visible only in %s'), + // TODO: Also link to object modifications + // $hook->linkToBranchedObject($this->translate('modifications'), $branch, $object, $auth), + $link + ))); + } + } +} diff --git a/library/Director/Web/Widget/BranchedObjectsHint.php b/library/Director/Web/Widget/BranchedObjectsHint.php new file mode 100644 index 0000000..d689178 --- /dev/null +++ b/library/Director/Web/Widget/BranchedObjectsHint.php @@ -0,0 +1,27 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Translation\TranslationHelper; +use gipfl\Web\Widget\Hint; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db\Branch\Branch; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class BranchedObjectsHint extends HtmlDocument +{ + use TranslationHelper; + + public function __construct(Branch $branch, Auth $auth) + { + if (! $branch->isBranch()) { + return; + } + $hook = Branch::requireHook(); + $this->add(Hint::info(Html::sprintf( + $this->translate('Showing a branched view, with potential changes being visible only in this %s'), + $hook->linkToBranch($branch, $auth, $this->translate('configuration branch')) + ))); + } +} diff --git a/library/Director/Web/Widget/Daemon/BackgroundDaemonState.php b/library/Director/Web/Widget/Daemon/BackgroundDaemonState.php new file mode 100644 index 0000000..03e76b2 --- /dev/null +++ b/library/Director/Web/Widget/Daemon/BackgroundDaemonState.php @@ -0,0 +1,57 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget\Daemon; + +use Icinga\Module\Director\Daemon\RunningDaemonInfo; +use Icinga\Module\Director\Db; + +class BackgroundDaemonState +{ + protected $db; + + /** @var RunningDaemonInfo[] */ + protected $instances; + + public function __construct(Db $db) + { + $this->db = $db; + } + + public function isRunning() + { + foreach ($this->getInstances() as $instance) { + if ($instance->isRunning()) { + return true; + } + } + + return false; + } + + protected function getInstances() + { + if ($this->instances === null) { + $this->instances = $this->fetchInfo(); + } + + return $this->instances; + } + + /** + * @return RunningDaemonInfo[] + */ + protected function fetchInfo() + { + $db = $this->db->getDbAdapter(); + $daemons = $db->fetchAll( + $db->select()->from('director_daemon_info')->order('fqdn')->order('username')->order('pid') + ); + + $result = []; + foreach ($daemons as $info) { + $result[] = new RunningDaemonInfo($info); + } + + return $result; + } +} diff --git a/library/Director/Web/Widget/DeployedConfigInfoHeader.php b/library/Director/Web/Widget/DeployedConfigInfoHeader.php new file mode 100644 index 0000000..0e841f3 --- /dev/null +++ b/library/Director/Web/Widget/DeployedConfigInfoHeader.php @@ -0,0 +1,101 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use Icinga\Module\Director\Db\Branch\Branch; +use ipl\Html\HtmlDocument; +use Icinga\Module\Director\Core\DeploymentApiInterface; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Forms\DeployConfigForm; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Widget\NameValueTable; + +class DeployedConfigInfoHeader extends HtmlDocument +{ + use TranslationHelper; + + /** @var IcingaConfig */ + protected $config; + + /** @var int */ + protected $deploymentId; + + /** @var Db */ + protected $db; + + /** @var DeploymentApiInterface */ + protected $api; + + /** @var Branch */ + protected $branch; + + public function __construct( + IcingaConfig $config, + Db $db, + DeploymentApiInterface $api, + Branch $branch, + $deploymentId = null + ) { + $this->config = $config; + $this->db = $db; + $this->api = $api; + $this->branch = $branch; + if ($deploymentId) { + $this->deploymentId = (int) $deploymentId; + } + } + + /** + * @throws \Icinga\Exception\IcingaException + * @throws \Zend_Form_Exception + */ + protected function assemble() + { + $config = $this->config; + if ($this->branch->isBranch()) { + $deployForm = null; + } else { + $deployForm = DeployConfigForm::load() + ->setDb($this->db) + ->setApi($this->api) + ->setChecksum($config->getHexChecksum()) + ->setDeploymentId($this->deploymentId) + ->setAttrib('class', 'inline') + ->handleRequest(); + } + + $links = new NameValueTable(); + $links->addNameValueRow( + $this->translate('Actions'), + [ + $deployForm, + Html::tag('br'), + Link::create( + $this->translate('Last related activity'), + 'director/config/activity', + ['checksum' => $config->getLastActivityHexChecksum()], + ['class' => 'icon-clock', 'data-base-target' => '_next'] + ), + Html::tag('br'), + Link::create( + $this->translate('Diff with other config'), + 'director/config/diff', + ['left' => $config->getHexChecksum()], + ['class' => 'icon-flapping', 'data-base-target' => '_self'] + ) + ] + )->addNameValueRow( + $this->translate('Statistics'), + sprintf( + $this->translate('%d files rendered in %0.2fs'), + count($config->getFiles()), + $config->getDuration() / 1000 + ) + ); + + $this->add($links); + } +} diff --git a/library/Director/Web/Widget/DeploymentInfo.php b/library/Director/Web/Widget/DeploymentInfo.php new file mode 100644 index 0000000..110200f --- /dev/null +++ b/library/Director/Web/Widget/DeploymentInfo.php @@ -0,0 +1,169 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\HtmlDocument; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Icinga\Module\Director\StartupLogRenderer; +use Icinga\Util\Format; +use Icinga\Web\Request; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Icon; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use gipfl\IcingaWeb2\Widget\Tabs; + +class DeploymentInfo extends HtmlDocument +{ + use TranslationHelper; + + /** @var DirectorDeploymentLog */ + protected $deployment; + + /** @var IcingaConfig */ + protected $config; + + /** + * DeploymentInfo constructor. + * @param DirectorDeploymentLog $deployment + */ + public function __construct(DirectorDeploymentLog $deployment) + { + $this->deployment = $deployment; + if ($deployment->get('config_checksum') !== null) { + $this->config = IcingaConfig::load( + $deployment->get('config_checksum'), + $deployment->getConnection() + ); + } + } + + /** + * @param Auth $auth + * @param Request $request + * @return Tabs + */ + public function getTabs(Auth $auth, Request $request) + { + $dep = $this->deployment; + $tabs = new Tabs(); + $tabs->add('deployment', array( + 'label' => $this->translate('Deployment'), + 'url' => $request->getUrl() + ))->activate('deployment'); + + if ($dep->config_checksum !== null && $auth->hasPermission('director/showconfig')) { + $tabs->add('config', array( + 'label' => $this->translate('Config'), + 'url' => 'director/config/files', + 'urlParams' => array( + 'checksum' => $this->config->getHexChecksum(), + 'deployment_id' => $dep->id + ) + )); + } + + return $tabs; + } + + protected function createInfoTable() + { + $dep = $this->deployment; + $table = new NameValueTable(); + $table->addNameValuePairs([ + $this->translate('Deployment time') => $dep->start_time, + $this->translate('Sent to') => $dep->peer_identity, + ]); + if ($this->config !== null) { + $table->addNameValuePairs([ + $this->translate('Configuration') => $this->getConfigDetails(), + $this->translate('Duration') => $this->getDurationInfo(), + ]); + } + $table->addNameValuePairs([ + $this->translate('Stage name') => $dep->stage_name, + $this->translate('Startup') => $this->getStartupInfo() + ]); + + return $table; + } + + protected function getDurationInfo() + { + return sprintf( + $this->translate('Rendered in %0.2fs, deployed in %0.2fs'), + $this->config->getDuration() / 1000, + $this->deployment->duration_dump / 1000 + ); + } + + protected function getConfigDetails() + { + $cfg = $this->config; + $dep = $this->deployment; + + return [ + Link::create( + sprintf($this->translate('%d files'), $cfg->getFileCount()), + 'director/config/files', + [ + 'checksum' => $cfg->getHexChecksum(), + 'deployment_id' => $dep->id + ] + ), + ', ', + sprintf( + $this->translate('%d objects, %d templates, %d apply rules'), + $cfg->getObjectCount(), + $cfg->getTemplateCount(), + $cfg->getApplyCount() + ), + ', ', + Format::bytes($cfg->getSize()) + ]; + } + + protected function getStartupInfo() + { + $dep = $this->deployment; + if ($dep->startup_succeeded === null) { + if ($dep->stage_collected === null) { + return [$this->translate('Unknown, still waiting for config check outcome'), new Icon('spinner')]; + } else { + return [$this->translate('Unknown, failed to collect related information'), new Icon('help')]; + } + } elseif ($dep->startup_succeeded === 'y') { + return $this->colored('green', [$this->translate('Succeeded'), new Icon('ok')]); + } else { + return $this->colored('red', [$this->translate('Failed'), new Icon('cancel')]); + } + } + + protected function colored($color, array $content) + { + return Html::tag('div', ['style' => "color: $color;"], $content)->setSeparator(' '); + } + + public function render() + { + $this->add($this->createInfoTable()); + if ($this->deployment->get('startup_succeeded') !== null) { + $this->addStartupLog(); + } + + return parent::render(); + } + + protected function addStartupLog() + { + $this->add(Html::tag('h2', null, $this->translate('Startup Log'))); + $this->add( + Html::tag('pre', [ + 'class' => 'logfile' + ], new StartupLogRenderer($this->deployment)) + ); + } +} diff --git a/library/Director/Web/Widget/Documentation.php b/library/Director/Web/Widget/Documentation.php new file mode 100644 index 0000000..8665e30 --- /dev/null +++ b/library/Director/Web/Widget/Documentation.php @@ -0,0 +1,97 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use Icinga\Application\ApplicationBootstrap; +use Icinga\Application\Icinga; +use Icinga\Authentication\Auth; +use ipl\Html\Html; + +class Documentation +{ + use TranslationHelper; + + /** @var ApplicationBootstrap */ + protected $app; + + /** @var Auth */ + protected $auth; + + public function __construct(ApplicationBootstrap $app, Auth $auth) + { + $this->app = $app; + $this->auth = $auth; + } + + public static function link($label, $module, $chapter, $title = null) + { + $doc = new static(Icinga::app(), Auth::getInstance()); + return $doc->getModuleLink($label, $module, $chapter, $title); + } + + public function getModuleLink($label, $module, $chapter, $title = null) + { + if ($title !== null) { + $title = sprintf( + $this->translate('Click to read our documentation: %s'), + $title + ); + } + $linkToGitHub = false; + $baseParams = [ + 'class' => 'icon-book', + 'title' => $title, + ]; + if ($this->hasAccessToDocumentationModule()) { + return Link::create( + $label, + $this->getDirectorDocumentationUrl($chapter), + null, + ['data-base-target' => '_next'] + $baseParams + ); + } + + $baseParams['target'] = '_blank'; + if ($linkToGitHub) { + return Html::tag('a', [ + 'href' => $this->githubDocumentationUrl($module, $chapter), + ] + $baseParams, $label); + } + + return Html::tag('a', [ + 'href' => $this->icingaDocumentationUrl($module, $chapter), + ] + $baseParams, $label); + } + + protected function getDirectorDocumentationUrl($chapter) + { + return 'doc/module/director/chapter/' + . \preg_replace('/^\d+-/', '', \rawurlencode($chapter)); + } + + protected function githubDocumentationUrl($module, $chapter) + { + return sprintf( + "https://github.com/Icinga/icingaweb2-module-%s/blob/master/doc/%s.md", + \rawurlencode($module), + \rawurlencode($chapter) + ); + } + + protected function icingaDocumentationUrl($module, $chapter) + { + return sprintf( + 'https://icinga.com/docs/%s/latest/doc/%s/', + \rawurlencode($module), + \rawurlencode($chapter) + ); + } + + protected function hasAccessToDocumentationModule() + { + return $this->app->getModuleManager()->hasLoaded('doc') + && $this->auth->hasPermission('module/doc'); + } +} diff --git a/library/Director/Web/Widget/HealthCheckPluginOutput.php b/library/Director/Web/Widget/HealthCheckPluginOutput.php new file mode 100644 index 0000000..83ac102 --- /dev/null +++ b/library/Director/Web/Widget/HealthCheckPluginOutput.php @@ -0,0 +1,94 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlString; +use gipfl\Translation\TranslationHelper; +use Icinga\Module\Director\CheckPlugin\PluginState; +use Icinga\Module\Director\Health; + +class HealthCheckPluginOutput extends HtmlDocument +{ + use TranslationHelper; + + /** @var Health */ + protected $health; + + /** @var PluginState */ + protected $state; + + public function __construct(Health $health) + { + $this->state = new PluginState('OK'); + $this->health = $health; + $this->process(); + } + + protected function process() + { + $checks = $this->health->getAllChecks(); + + foreach ($checks as $check) { + $this->add([ + $title = Html::tag('h1', $check->getName()), + $ul = Html::tag('ul', ['class' => 'health-check-result']) + ]); + + $problems = $check->getProblemSummary(); + if (! empty($problems)) { + $badges = Html::tag('span', ['class' => 'title-badges']); + foreach ($problems as $state => $count) { + $badges->add(Html::tag('span', [ + 'class' => ['badge', 'state-' . strtolower($state)], + 'title' => sprintf( + $this->translate('%s: %d'), + $this->translate($state), + $count + ), + ], $count)); + } + $title->add($badges); + } + + foreach ($check->getResults() as $result) { + $state = $result->getState()->getName(); + $ul->add(Html::tag('li', [ + 'class' => 'state state-' . strtolower($state) + ], $this->highlightNames($result->getOutput()))->setSeparator(' ')); + } + $this->state->raise($check->getState()); + } + } + + public function getState() + { + return $this->state; + } + + protected function colorizeState($state) + { + return Html::tag('span', ['class' => 'badge state-' . strtolower($state)], $state); + } + + protected function highlightNames($string) + { + $string = Html::escape($string); + return new HtmlString(preg_replace_callback( + "/'([^']+)'/", + [$this, 'highlightName'], + $string + )); + } + + protected function highlightName($match) + { + return '"' . Html::tag('strong', $match[1]) . '"'; + } + + protected function getColorized($match) + { + return $this->colorizeState($match[1]); + } +} diff --git a/library/Director/Web/Widget/IcingaConfigDiff.php b/library/Director/Web/Widget/IcingaConfigDiff.php new file mode 100644 index 0000000..800f1d9 --- /dev/null +++ b/library/Director/Web/Widget/IcingaConfigDiff.php @@ -0,0 +1,58 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Diff\HtmlRenderer\SideBySideDiff; +use gipfl\Diff\PhpDiff; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use ipl\Html\ValidHtml; + +class IcingaConfigDiff extends HtmlDocument +{ + public function __construct(IcingaConfig $left, IcingaConfig $right) + { + foreach (static::getDiffs($left, $right) as $filename => $diff) { + $this->add([ + Html::tag('h3', $filename), + $diff + ]); + } + } + + /** + * @param IcingaConfig $oldConfig + * @param IcingaConfig $newConfig + * @return ValidHtml[] + */ + public static function getDiffs(IcingaConfig $oldConfig, IcingaConfig $newConfig) + { + $oldFileNames = $oldConfig->getFileNames(); + $newFileNames = $newConfig->getFileNames(); + + $fileNames = array_merge($oldFileNames, $newFileNames); + + $diffs = []; + foreach ($fileNames as $filename) { + if (in_array($filename, $oldFileNames)) { + $left = $oldConfig->getFile($filename)->getContent(); + } else { + $left = ''; + } + + if (in_array($filename, $newFileNames)) { + $right = $newConfig->getFile($filename)->getContent(); + } else { + $right = ''; + } + if ($left === $right) { + continue; + } + + $diffs[$filename] = new SideBySideDiff(new PhpDiff($left, $right)); + } + + return $diffs; + } +} diff --git a/library/Director/Web/Widget/IcingaObjectInspection.php b/library/Director/Web/Widget/IcingaObjectInspection.php new file mode 100644 index 0000000..61f3567 --- /dev/null +++ b/library/Director/Web/Widget/IcingaObjectInspection.php @@ -0,0 +1,254 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\PlainObjectRenderer; +use Icinga\Module\Director\Web\Table\DbHelper; +use stdClass; + +class IcingaObjectInspection extends BaseHtmlElement +{ + use DbHelper; + use TranslationHelper; + + protected $tag = 'div'; + + /** @var Db */ + protected $db; + + /** @var stdClass */ + protected $object; + + public function __construct(stdClass $object, Db $db) + { + $this->object = $object; + $this->db = $db; + } + + /** + * @throws \Icinga\Exception\IcingaException + */ + protected function assemble() + { + $attrs = $this->object->attrs; + if (isset($attrs->source_location)) { + $this->renderSourceLocation($attrs->source_location); + } + if (isset($attrs->last_check_result)) { + $this->renderLastCheckResult($attrs->last_check_result); + } + + $this->renderObjectAttributes($attrs); + // $this->add(Html::tag('pre', null, PlainObjectRenderer::render($this->object))); + } + + /** + * @param $result + * @throws \Icinga\Exception\IcingaException + */ + protected function renderLastCheckResult($result) + { + $this->add(Html::tag('h2', null, $this->translate('Last Check Result'))); + $this->renderCheckResultDetails($result); + if (property_exists($result, 'command')) { + $this->renderExecutedCommand($result->command); + } + } + + /** + * @param array|string $command + * + * @throws \Icinga\Exception\IcingaException + */ + protected function renderExecutedCommand($command) + { + if (is_array($command)) { + $command = implode(' ', array_map('escapeshellarg', $command)); + } + $this->add([ + Html::tag('h3', null, 'Executed Command'), + $this->formatConsole($command) + ]); + } + + protected function renderCheckResultDetails($result) + { + } + + /** + * @param $attrs + * @throws \Icinga\Exception\IcingaException + */ + protected function renderObjectAttributes($attrs) + { + $blacklist = [ + 'last_check_result', + 'source_location', + 'templates', + ]; + + $linked = [ + 'check_command', + 'groups', + ]; + + $info = new NameValueTable(); + foreach ($attrs as $key => $value) { + if (in_array($key, $blacklist)) { + continue; + } + if ($key === 'groups') { + $info->addNameValueRow($key, $this->linkGroups($value)); + } elseif (in_array($key, $linked)) { + $info->addNameValueRow($key, $this->renderLinkedObject($key, $value)); + } else { + $info->addNameValueRow($key, PlainObjectRenderer::render($value)); + } + } + + $this->add([ + Html::tag('h2', null, 'Object Properties'), + $info + ]); + } + + /** + * @param $key + * @param $objectName + * @return Link|Link[] + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function renderLinkedObject($key, $objectName) + { + $keys = [ + 'check_command' => ['CheckCommand', 'CheckCommands'], + 'event_command' => ['EventCommand', 'EventCommands'], + 'notification_command' => ['NotificationCommand', 'NotificationCommands'], + ]; + $type = $keys[$key]; + + if ($key === 'groups') { + return $this->linkGroups($objectName); + } else { + $singular = $type[0]; + $plural = $type[1]; + + return Link::create($objectName, 'director/inspect/object', [ + 'type' => $singular, + 'plural' => $plural, + 'name' => $objectName + ]); + } + } + + /** + * @param $groups + * @return Link[] + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function linkGroups($groups) + { + if ($groups === null) { + return []; + } + + $singular = $this->object->type . 'Group'; + $plural = $singular . "s"; + + $links = []; + + foreach ($groups as $name) { + $links[] = Link::create($name, 'director/inspect/object', [ + 'type' => $singular, + 'plural' => $plural, + 'name' => $name + ]); + } + + return $links; + } + + /** + * @param stdClass $source + * @throws \Icinga\Exception\IcingaException + */ + protected function renderSourceLocation(stdClass $source) + { + $findRelative = 'api/packages/director'; + $this->add(Html::tag('h2')->add('Source Location')); + $pos = strpos($source->path, $findRelative); + + if (false === $pos) { + $this->add(Html::tag('p', null, Html::sprintf( + 'The configuration for this object has not been rendered by' + . ' Icinga Director. You can find it on line %s in %s.', + Html::tag('strong', null, $source->first_line), + Html::tag('strong', null, $source->path) + ))); + } else { + $relativePath = substr($source->path, $pos + strlen($findRelative) + 1); + $parts = explode('/', $relativePath); + $stageName = array_shift($parts); + $relativePath = implode('/', $parts); + $source->director_relative = $relativePath; + $deployment = $this->loadDeploymentForStage($stageName); + + $this->add(Html::tag('p')->add(Html::sprintf( + 'The configuration for this object has been rendered by Icinga' + . ' Director %s to %s', + DateFormatter::timeAgo(strtotime($deployment->start_time, false)), + $this->linkToSourceLocation($deployment, $source) + ))); + } + } + + protected function loadDeploymentForStage($stageName) + { + $db = $this->db->getDbAdapter(); + $query = $db->select()->from( + ['dl' => 'director_deployment_log'], + ['id', 'start_time', 'config_checksum'] + )->where('stage_name = ?', $stageName)->order('id DESC')->limit(1); + + return $db->fetchRow($query); + } + + /** + * @param $deployment + * @param $source + * @return Link + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function linkToSourceLocation($deployment, $source) + { + $filename = $source->director_relative; + + return Link::create( + sprintf('%s:%s', $filename, $source->first_line), + 'director/config/file', + [ + 'config_checksum' => $this->getChecksum($deployment->config_checksum), + 'deployment_id' => $deployment->id, + 'backTo' => 'deployment', + 'file_path' => $filename, + 'highlight' => $source->first_line, + 'highlightSeverity' => 'ok' + ] + ); + } + + protected function formatConsole($output) + { + return Html::tag('pre', ['class' => 'logfile'], $output); + } +} diff --git a/library/Director/Web/Widget/ImportSourceDetails.php b/library/Director/Web/Widget/ImportSourceDetails.php new file mode 100644 index 0000000..32eef7f --- /dev/null +++ b/library/Director/Web/Widget/ImportSourceDetails.php @@ -0,0 +1,83 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Web\Widget\Hint; +use ipl\Html\HtmlDocument; +use Icinga\Module\Director\Forms\ImportCheckForm; +use Icinga\Module\Director\Forms\ImportRunForm; +use Icinga\Module\Director\Objects\ImportSource; +use ipl\Html\Html; +use gipfl\Translation\TranslationHelper; + +class ImportSourceDetails extends HtmlDocument +{ + use TranslationHelper; + + protected $source; + + public function __construct(ImportSource $source) + { + $this->source = $source; + } + + protected function assemble() + { + $source = $this->source; + $description = $source->get('description'); + if ($description !== null && strlen($description)) { + $this->add(Html::tag('p', null, $description)); + } + + switch ($source->get('import_state')) { + case 'unknown': + $this->add(Hint::warning($this->translate( + "It's currently unknown whether we are in sync with this Import Source." + . ' You should either check for changes or trigger a new Import Run.' + ))); + break; + case 'in-sync': + $this->add(Hint::ok(sprintf( + $this->translate( + 'This Import Source was last found to be in sync at %s.' + ), + $source->last_attempt + ))); + // TODO: check whether... + // - there have been imports since then, differing from former ones + // - there have been activities since then + break; + case 'pending-changes': + $this->add(Hint::warning($this->translate( + 'There are pending changes for this Import Source. You should trigger a new' + . ' Import Run.' + ))); + break; + case 'failing': + $this->add(Hint::error(sprintf( + $this->translate( + 'This Import Source failed when last checked at %s: %s' + ), + $source->last_attempt, + $source->last_error_message + ))); + break; + default: + $this->add(Hint::error(sprintf( + $this->translate('This Import Source has an invalid state: %s'), + $source->get('import_state') + ))); + } + + $this->add( + ImportCheckForm::load() + ->setImportSource($source) + ->handleRequest() + ); + $this->add( + ImportRunForm::load() + ->setImportSource($source) + ->handleRequest() + ); + } +} diff --git a/library/Director/Web/Widget/InspectPackages.php b/library/Director/Web/Widget/InspectPackages.php new file mode 100644 index 0000000..f9b8864 --- /dev/null +++ b/library/Director/Web/Widget/InspectPackages.php @@ -0,0 +1,174 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaEndpoint; +use ipl\Html\Html; +use ipl\Html\Table; + +class InspectPackages +{ + use TranslationHelper; + + /** @var Db */ + protected $db; + + /** @var string */ + protected $baseUrl; + + public function __construct(Db $db, $baseUrl) + { + $this->db = $db; + $this->baseUrl = $baseUrl; + } + + public function getContent(IcingaEndpoint $endpoint = null, $package = null, $stage = null, $file = null) + { + if ($endpoint === null) { + return $this->getRootEndpoints(); + } elseif ($package === null) { + return $this->getPackages($endpoint); + } elseif ($stage === null) { + return $this->getStages($endpoint, $package); + } elseif ($file === null) { + return $this->getFiles($endpoint, $package, $stage); + } else { + return $this->getFile($endpoint, $package, $stage, $file); + } + } + + public function getTitle(IcingaEndpoint $endpoint = null, $package = null, $stage = null, $file = null) + { + if ($endpoint === null) { + return $this->translate('Endpoint in your Root Zone'); + } elseif ($package === null) { + return \sprintf($this->translate('Packages on Endpoint: %s'), $endpoint->getObjectName()); + } elseif ($stage === null) { + return \sprintf($this->translate('Stages in Package: %s'), $package); + } elseif ($file === null) { + return \sprintf($this->translate('Files in Stage: %s'), $stage); + } else { + return \sprintf($this->translate('File Content: %s'), $file); + } + } + + public function getBreadCrumb(IcingaEndpoint $endpoint = null, $package = null, $stage = null) + { + $parts = [ + 'endpoint' => $endpoint === null ? null : $endpoint->getObjectName(), + 'package' => $package, + 'stage' => $stage, + ]; + + $params = []; + // No root zone link for now: + // $result = [Link::create($this->translate('Root Zone'), $this->baseUrl)]; + $result = [Html::tag('a', ['href' => '#'], $this->translate('Root Zone'))]; + foreach ($parts as $name => $value) { + if ($value === null) { + break; + } + $params[$name] = $value; + $result[] = Link::create($value, $this->baseUrl, $params); + } + + return Html::tag('ul', ['class' => 'breadcrumb'], Html::wrapEach($result, 'li')); + } + + protected function getRootEndpoints() + { + $table = $this->prepareTable(); + foreach ($this->db->getEndpointNamesInDeploymentZone() as $name) { + $table->add(Table::row([ + Link::create($name, $this->baseUrl, [ + 'endpoint' => $name, + ]) + ])); + } + + return $table; + } + + protected function getPackages(IcingaEndpoint $endpoint) + { + $table = $this->prepareTable(); + $api = $endpoint->api(); + foreach ($api->getPackages() as $package) { + $table->add(Table::row([ + Link::create($package->name, $this->baseUrl, [ + 'endpoint' => $endpoint->getObjectName(), + 'package' => $package->name, + ]) + ])); + } + + return $table; + } + + protected function getStages(IcingaEndpoint $endpoint, $packageName) + { + $table = $this->prepareTable(); + $api = $endpoint->api(); + foreach ($api->getPackages() as $package) { + if ($package->name !== $packageName) { + continue; + } + foreach ($package->stages as $stage) { + $label = [$stage]; + if ($stage === $package->{'active-stage'}) { + $label[] = Html::tag('small', [' (', $this->translate('active'), ')']); + } + + $table->add(Table::row([ + Link::create($label, $this->baseUrl, [ + 'endpoint' => $endpoint->getObjectName(), + 'package' => $package->name, + 'stage' => $stage + ]) + ])); + } + } + + return $table; + } + + protected function getFiles(IcingaEndpoint $endpoint, $package, $stage) + { + $table = $this->prepareTable(); + $table->getAttributes()->set('data-base-target', '_next'); + foreach ($endpoint->api()->listStageFiles($stage, $package) as $filename) { + $table->add($table->row([ + Link::create($filename, $this->baseUrl, [ + 'endpoint' => $endpoint->getObjectName(), + 'package' => $package, + 'stage' => $stage, + 'file' => $filename + ]) + ])); + } + + return $table; + } + + protected function getFile(IcingaEndpoint $endpoint, $package, $stage, $file) + { + return Html::tag('pre', $endpoint->api()->getStagedFile($stage, $file, $package)); + } + + protected function prepareTable($headerCols = []) + { + $table = new Table(); + $table->addAttributes([ + 'class' => ['common-table', 'table-row-selectable'], + 'data-base-target' => '_self' + ]); + if (! empty($headerCols)) { + $table->add($table::row($headerCols, null, 'th')); + } + + return $table; + } +} diff --git a/library/Director/Web/Widget/JobDetails.php b/library/Director/Web/Widget/JobDetails.php new file mode 100644 index 0000000..3a530a2 --- /dev/null +++ b/library/Director/Web/Widget/JobDetails.php @@ -0,0 +1,69 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use ipl\Html\HtmlDocument; +use Icinga\Module\Director\Objects\DirectorJob; +use ipl\Html\Html; +use gipfl\Translation\TranslationHelper; + +class JobDetails extends HtmlDocument +{ + use TranslationHelper; + + /** + * JobDetails constructor. + * @param DirectorJob $job + * @throws \Icinga\Exception\NotFoundError + */ + public function __construct(DirectorJob $job) + { + $runInterval = $job->get('run_interval'); + if ($job->hasBeenDisabled()) { + $this->add(Hint::error(sprintf( + $this->translate( + 'This job would run every %ds. It has been disabled and will' + . ' therefore not be executed as scheduled' + ), + $runInterval + ))); + } else { + //$class = $job->job(); echo $class::getDescription() + $msg = $job->isPending() + ? sprintf( + $this->translate('This job runs every %ds and is currently pending'), + $runInterval + ) + : sprintf( + $this->translate('This job runs every %ds.'), + $runInterval + ); + $this->add(Html::tag('p', null, $msg)); + } + + $tsLastAttempt = $job->get('ts_last_attempt'); + if ($tsLastAttempt) { + $ts = \strtotime($tsLastAttempt); + $timeAgo = Html::tag('span', [ + 'class' => 'time-ago', + 'title' => DateFormatter::formatDateTime($ts) + ], DateFormatter::timeAgo($ts)); + if ($job->get('last_attempt_succeeded') === 'y') { + $this->add(Hint::ok(Html::sprintf( + $this->translate('The last attempt succeeded %s'), + $timeAgo + ))); + } else { + $this->add(Hint::error(Html::sprintf( + $this->translate('The last attempt failed %s: %s'), + $timeAgo, + $job->get('last_error_message') + ))); + } + } else { + $this->add(Hint::warning($this->translate('This job has not been executed yet'))); + } + } +} diff --git a/library/Director/Web/Widget/ListItem.php b/library/Director/Web/Widget/ListItem.php new file mode 100644 index 0000000..ec326cc --- /dev/null +++ b/library/Director/Web/Widget/ListItem.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\ValidHtml; + +class ListItem extends BaseHtmlElement +{ + protected $contentSeparator = "\n"; + + /** + * @param ValidHtml|array|string $content + * @param Attributes|array $attributes + * + * @return $this + */ + public function addItem($content, $attributes = null) + { + return $this->add( + Html::tag('li', $attributes, $content) + ); + } +} diff --git a/library/Director/Web/Widget/NotInBranchedHint.php b/library/Director/Web/Widget/NotInBranchedHint.php new file mode 100644 index 0000000..222934b --- /dev/null +++ b/library/Director/Web/Widget/NotInBranchedHint.php @@ -0,0 +1,23 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use gipfl\Translation\TranslationHelper; +use gipfl\Web\Widget\Hint; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db\Branch\Branch; +use ipl\Html\Html; + +class NotInBranchedHint extends Hint +{ + use TranslationHelper; + + public function __construct($forbiddenAction, Branch $branch, Auth $auth) + { + parent::__construct(Html::sprintf( + $this->translate('%s is not available while being in a Configuration Branch: %s'), + $forbiddenAction, + Branch::requireHook()->linkToBranch($branch, $auth, $branch->getName()) + ), 'info'); + } +} diff --git a/library/Director/Web/Widget/OrderedList.php b/library/Director/Web/Widget/OrderedList.php new file mode 100644 index 0000000..8f888de --- /dev/null +++ b/library/Director/Web/Widget/OrderedList.php @@ -0,0 +1,8 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +class OrderedList extends AbstractList +{ + protected $tag = 'ol'; +} diff --git a/library/Director/Web/Widget/ShowConfigFile.php b/library/Director/Web/Widget/ShowConfigFile.php new file mode 100644 index 0000000..77d32cf --- /dev/null +++ b/library/Director/Web/Widget/ShowConfigFile.php @@ -0,0 +1,106 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use ipl\Html\HtmlDocument; +use Icinga\Module\Director\IcingaConfig\IcingaConfigFile; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; + +class ShowConfigFile extends HtmlDocument +{ + use TranslationHelper; + + protected $file; + + protected $highlight; + + protected $highlightSeverity; + + public function __construct( + IcingaConfigFile $file, + $highlight = null, + $highlightSeverity = null + ) { + $this->file = $file; + $this->highlight = $highlight; + $this->highlightSeverity = $highlightSeverity; + } + + /** + * @throws \Icinga\Exception\IcingaException + */ + protected function assemble() + { + $source = $this->linkObjects(Html::escape($this->file->getContent())); + if ($this->highlight) { + $source = $this->highlight( + $source, + $this->highlight, + $this->highlightSeverity + ); + } + + $this->add(Html::tag( + 'pre', + ['class' => 'generated-config'], + new HtmlString($source) + )); + } + + /** + * @param $match + * @return string + * @throws \Icinga\Exception\IcingaException + * @throws \Icinga\Exception\ProgrammingError + */ + protected function linkObject($match) + { + if ($match[2] === 'Service') { + return $match[0]; + } + $controller = $match[2]; + + if ($match[2] === 'CheckCommand') { + $controller = 'command'; + } + + $name = $this->decode($match[3]); + return sprintf( + '%s %s "%s" {', + $match[1], + $match[2], + Link::create( + $name, + 'director/' . $controller, + ['name' => $name], + ['data-base-target' => '_next'] + ) + ); + } + + protected function decode($str) + { + return htmlspecialchars_decode($str, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5); + } + + protected function linkObjects($config) + { + $pattern = '/^(object|template)\s([A-Z][A-Za-z]*?)\s"(.+?)"\s{/m'; + + return preg_replace_callback( + $pattern, + [$this, 'linkObject'], + $config + ); + } + + protected function highlight($what, $line, $severity) + { + $lines = explode("\n", $what); + $lines[$line - 1] = '<span class="highlight ' . $severity . '">' . $lines[$line - 1] . '</span>'; + return implode("\n", $lines); + } +} diff --git a/library/Director/Web/Widget/SyncRunDetails.php b/library/Director/Web/Widget/SyncRunDetails.php new file mode 100644 index 0000000..408e8f6 --- /dev/null +++ b/library/Director/Web/Widget/SyncRunDetails.php @@ -0,0 +1,129 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +use Icinga\Module\Director\Objects\DirectorActivityLog; +use ipl\Html\HtmlDocument; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\SyncRun; +use gipfl\IcingaWeb2\Link; +use gipfl\Translation\TranslationHelper; +use gipfl\IcingaWeb2\Widget\NameValueTable; +use function sprintf; + +class SyncRunDetails extends NameValueTable +{ + use TranslationHelper; + + const URL_ACTIVITIES = 'director/config/activities'; + + /** @var SyncRun */ + protected $run; + + public function __construct(SyncRun $run) + { + $this->run = $run; + $this->getAttributes()->add('data-base-target', '_next'); // eigentlich nur runSummary + $this->addNameValuePairs([ + $this->translate('Start time') => $run->get('start_time'), + $this->translate('Duration') => sprintf('%.2fs', $run->get('duration_ms') / 1000), + $this->translate('Activity') => $this->runSummary($run) + ]); + } + + /** + * @param SyncRun $run + * @return array + */ + protected function runSummary(SyncRun $run) + { + $html = []; + $total = $run->countActivities(); + if ($total === 0) { + $html[] = $this->translate('No changes have been made'); + } else { + if ($total === 1) { + $html[] = $this->translate('One object has been modified'); + } else { + $html[] = sprintf( + $this->translate('%s objects have been modified'), + $total + ); + } + + /** @var Db $db */ + $db = $run->getConnection(); + $formerId = $db->fetchActivityLogIdByChecksum($run->get('last_former_activity')); + if ($formerId === null) { + return $html; + } + $lastId = $db->fetchActivityLogIdByChecksum($run->get('last_related_activity')); + + if ($formerId !== $lastId) { + $idRangeEx = sprintf( + 'id>%d&id<=%d', + $formerId, + $lastId + ); + } else { + $idRangeEx = null; + } + + $links = new HtmlDocument(); + $links->setSeparator(', '); + $links->add([ + $this->activitiesLink( + 'objects_created', + $this->translate('%d created'), + DirectorActivityLog::ACTION_CREATE, + $idRangeEx + ), + $this->activitiesLink( + 'objects_modified', + $this->translate('%d modified'), + DirectorActivityLog::ACTION_MODIFY, + $idRangeEx + ), + $this->activitiesLink( + 'objects_deleted', + $this->translate('%d deleted'), + DirectorActivityLog::ACTION_DELETE, + $idRangeEx + ), + ]); + + if ($idRangeEx && count($links) > 1) { + $links->add(new Link( + $this->translate('Show all actions'), + self::URL_ACTIVITIES, + ['idRangeEx' => $idRangeEx] + )); + } + + if (! $links->isEmpty()) { + $html[] = ': '; + $html[] = $links; + } + } + + return $html; + } + + protected function activitiesLink($key, $label, $action, $rangeFilter) + { + $count = $this->run->get($key); + if ($count > 0) { + if ($rangeFilter) { + return new Link( + sprintf($label, $count), + self::URL_ACTIVITIES, + ['action' => $action, 'idRangeEx' => $rangeFilter] + ); + } + + return sprintf($label, $count); + } + + return null; + } +} diff --git a/library/Director/Web/Widget/UnorderedList.php b/library/Director/Web/Widget/UnorderedList.php new file mode 100644 index 0000000..f01dbe3 --- /dev/null +++ b/library/Director/Web/Widget/UnorderedList.php @@ -0,0 +1,8 @@ +<?php + +namespace Icinga\Module\Director\Web\Widget; + +class UnorderedList extends AbstractList +{ + protected $tag = 'ul'; +} |