diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:39:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:39:39 +0000 |
commit | 8ca6cc32b2c789a3149861159ad258f2cb9491e3 (patch) | |
tree | 2492de6f1528dd44eaa169a5c1555026d9cb75ec /modules | |
parent | Initial commit. (diff) | |
download | icingaweb2-8ca6cc32b2c789a3149861159ad258f2cb9491e3.tar.xz icingaweb2-8ca6cc32b2c789a3149861159ad258f2cb9491e3.zip |
Adding upstream version 2.11.4.upstream/2.11.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
470 files changed, 56850 insertions, 0 deletions
diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php new file mode 100644 index 0000000..e841c41 --- /dev/null +++ b/modules/doc/application/controllers/IcingawebController.php @@ -0,0 +1,62 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Module\Doc\DocController; + +class IcingawebController extends DocController +{ + /** + * Get the path to Icinga Web 2's documentation + * + * @return string + * + * @throws \Icinga\Exception\Http\HttpNotFoundException If Icinga Web 2's documentation is not available + */ + protected function getPath() + { + $path = Icinga::app()->getBaseDir('doc'); + if (is_dir($path)) { + return $path; + } + if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) { + if (is_dir($path)) { + return $path; + } + } + $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available')); + } + + /** + * View the toc of Icinga Web 2's documentation + */ + public function tocAction() + { + $this->renderToc($this->getPath(), 'Icinga Web 2', 'doc/icingaweb/chapter'); + } + + /** + * View a chapter of Icinga Web 2's documentation + * + * @throws \Icinga\Exception\MissingParameterException If the required parameter 'chapter' is missing + */ + public function chapterAction() + { + $chapter = $this->params->getRequired('chapter'); + $this->renderChapter( + $this->getPath(), + $chapter, + 'doc/icingaweb/chapter' + ); + } + + /** + * View Icinga Web 2's documentation as PDF + */ + public function pdfAction() + { + $this->renderPdf($this->getPath(), 'Icinga Web 2', 'doc/icingaweb/chapter'); + } +} diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php new file mode 100644 index 0000000..3ff5aa1 --- /dev/null +++ b/modules/doc/application/controllers/IndexController.php @@ -0,0 +1,27 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Controllers; + +use Icinga\Module\Doc\DocController; +use Icinga\Web\Url; + +/** + * Documentation module index + */ +class IndexController extends DocController +{ + /** + * Documentation module landing page + * + * Lists documentation links + */ + public function indexAction() + { + $this->getTabs()->add('documentation', array( + 'active' => true, + 'title' => $this->translate('Documentation', 'Tab title'), + 'url' => Url::fromRequest() + )); + } +} diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php new file mode 100644 index 0000000..47dfb1c --- /dev/null +++ b/modules/doc/application/controllers/ModuleController.php @@ -0,0 +1,206 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Controllers; + +use finfo; +use SplFileInfo; +use Icinga\Application\Icinga; +use Icinga\Module\Doc\DocController; +use Icinga\Module\Doc\Exception\DocException; +use Icinga\Web\Url; + +class ModuleController extends DocController +{ + /** + * Get the path to a module documentation + * + * @param string $module The name of the module + * @param string $default The default path + * @param bool $suppressErrors Whether to not throw an exception if the module documentation is not available + * + * @return string|null Path to the documentation or null if the module documentation is not available + * and errors are suppressed + * + * @throws \Icinga\Exception\Http\HttpNotFoundException If the module documentation is not available and errors + * are not suppressed + */ + protected function getPath($module, $default, $suppressErrors = false) + { + if (is_dir($default)) { + return $default; + } + if (($path = $this->Config()->get('documentation', 'modules')) !== null) { + $path = str_replace('{module}', $module, $path); + if (is_dir($path)) { + return $path; + } + } + if ($suppressErrors) { + return null; + } + $this->httpNotFound($this->translate('Documentation for module \'%s\' is not available'), $module); + } + + /** + * List modules which are enabled and having the 'doc' directory + */ + public function indexAction() + { + $moduleManager = Icinga::app()->getModuleManager(); + $modules = array(); + foreach ($moduleManager->listInstalledModules() as $module) { + $path = $this->getPath($module, $moduleManager->getModuleDir($module, '/doc'), true); + if ($path !== null) { + $modules[] = $moduleManager->getModule($module, false); + } + } + $this->view->modules = $modules; + $this->getTabs()->add('module-documentation', array( + 'active' => true, + 'title' => $this->translate('Module Documentation', 'Tab title'), + 'url' => Url::fromRequest() + )); + } + + /** + * Assert that the given module is installed + * + * @param string $moduleName + * + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed + */ + protected function assertModuleInstalled($moduleName) + { + $moduleManager = Icinga::app()->getModuleManager(); + if (! $moduleManager->hasInstalled($moduleName)) { + $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName); + } + } + + /** + * View the toc of a module's documentation + * + * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed + * @see assertModuleInstalled() + */ + public function tocAction() + { + $module = $this->params->getRequired('moduleName'); + $this->assertModuleInstalled($module); + $moduleManager = Icinga::app()->getModuleManager(); + $name = $moduleManager->getModule($module, false)->getTitle(); + try { + $this->renderToc( + $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')), + $name, + 'doc/module/chapter', + array('moduleName' => $module) + ); + } catch (DocException $e) { + $this->httpNotFound($e->getMessage()); + } + } + + /** + * View a chapter of a module's documentation + * + * @throws \Icinga\Exception\MissingParameterException If one of the required parameters 'moduleName' and + * 'chapter' is empty + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed + * @see assertModuleInstalled() + */ + public function chapterAction() + { + $module = $this->params->getRequired('moduleName'); + $this->assertModuleInstalled($module); + $chapter = $this->params->getRequired('chapter'); + $this->view->moduleName = $module; + try { + $this->renderChapter( + $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')), + $chapter, + 'doc/module/chapter', + 'doc/module/img', + array('moduleName' => $module) + ); + } catch (DocException $e) { + $this->httpNotFound($e->getMessage()); + } + } + + /** + * Deliver images + */ + public function imageAction() + { + $module = $this->params->getRequired('moduleName'); + $image = $this->params->getRequired('image'); + $docPath = $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')); + $imagePath = realpath($docPath . '/' . $image); + if ($imagePath === false || substr($imagePath, 0, strlen($docPath)) !== $docPath) { + $this->httpNotFound('%s does not exist', $image); + } + + $this->_helper->viewRenderer->setNoRender(true); + $this->_helper->layout()->disableLayout(); + + $imageInfo = new SplFileInfo($imagePath); + + $etag = md5($imageInfo->getMTime() . $imagePath); + $lastModified = gmdate('D, d M Y H:i:s T', $imageInfo->getMTime()); + $match = false; + + if (isset($_SERER['HTTP_IF_NONE_MATCH'])) { + $ifNoneMatch = explode(', ', stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])); + foreach ($ifNoneMatch as $tag) { + if ($tag === $etag) { + $match = true; + break; + } + } + } elseif (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + $lastModifiedSince = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']); + if ($lastModifiedSince === $lastModified) { + $match = true; + } + } + + $this->getResponse() + ->setHeader('ETag', $etag) + ->setHeader('Cache-Control', 'no-transform,public,max-age=3600,must-revalidate', true); + + if ($match) { + $this->getResponse()->setHttpResponseCode(304); + } else { + $finfo = new finfo(); + $this->getResponse() + ->setHeader('Content-Type', $finfo->file($imagePath, FILEINFO_MIME_TYPE)) + ->setHeader('Content-Length', $imageInfo->getSize()) + ->sendHeaders(); + + ob_end_clean(); + readfile($imagePath); + } + } + + /** + * View a module's documentation as PDF + * + * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed + * @see assertModuleInstalled() + */ + public function pdfAction() + { + $module = $this->params->getRequired('moduleName'); + $this->assertModuleInstalled($module); + $this->renderPdf( + $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')), + $module, + 'doc/module/chapter', + array('moduleName' => $module) + ); + } +} diff --git a/modules/doc/application/controllers/SearchController.php b/modules/doc/application/controllers/SearchController.php new file mode 100644 index 0000000..6ae2b14 --- /dev/null +++ b/modules/doc/application/controllers/SearchController.php @@ -0,0 +1,97 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Module\Doc\DocController; +use Icinga\Module\Doc\DocParser; +use Icinga\Module\Doc\Exception\DocException; +use Icinga\Module\Doc\Renderer\DocSearchRenderer; +use Icinga\Module\Doc\Search\DocSearch; +use Icinga\Module\Doc\Search\DocSearchIterator; + +class SearchController extends DocController +{ + /** + * Render search + */ + public function indexAction() + { + $parser = new DocParser($this->getWebPath()); + $search = new DocSearchRenderer( + new DocSearchIterator( + $parser->getDocTree()->getIterator(), + new DocSearch($this->params->get('q')) + ) + ); + $search->setUrl('doc/icingaweb/chapter'); + if (strlen($this->params->get('q')) < 3) { + $this->view->searches = array(); + return; + } + $searches = array( + 'Icinga Web 2' => $search + ); + foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $module) { + if (($path = $this->getModulePath($module)) !== null) { + try { + $parser = new DocParser($path); + $search = new DocSearchRenderer( + new DocSearchIterator( + $parser->getDocTree()->getIterator(), + new DocSearch($this->params->get('q')) + ) + ); + } catch (DocException $e) { + continue; + } + $search + ->setUrl('doc/module/chapter') + ->setUrlParams(array('moduleName' => $module)); + $searches[$module] = $search; + } + } + $this->view->searches = $searches; + } + + /** + * Get the path to a module's documentation + * + * @param string $module + * + * @return string|null + */ + protected function getModulePath($module) + { + if (is_dir(($path = Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')))) { + return $path; + } + if (($path = $this->Config()->get('documentation', 'modules')) !== null) { + $path = str_replace('{module}', $module, $path); + if (is_dir($path)) { + return $path; + } + } + return null; + } + + /** + * Get the path to Icinga Web 2's documentation + * + * @return string + */ + protected function getWebPath() + { + $path = Icinga::app()->getBaseDir('doc'); + if (is_dir($path)) { + return $path; + } + if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) { + if (is_dir($path)) { + return $path; + } + } + $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available')); + } +} diff --git a/modules/doc/application/controllers/StyleController.php b/modules/doc/application/controllers/StyleController.php new file mode 100644 index 0000000..5890367 --- /dev/null +++ b/modules/doc/application/controllers/StyleController.php @@ -0,0 +1,41 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Web\Controller; +use Icinga\Web\Widget; + +class StyleController extends Controller +{ + public function guideAction() + { + $this->view->tabs = $this->tabs()->activate('guide'); + } + + public function fontAction() + { + $this->view->tabs = $this->tabs()->activate('font'); + $confFile = Icinga::app()->getApplicationDir('fonts/fontello-ifont/config.json'); + $this->view->font = json_decode(file_get_contents($confFile)); + } + + protected function tabs() + { + return Widget::create('tabs')->add( + 'guide', + array( + 'label' => $this->translate('Style Guide'), + 'url' => 'doc/style/guide' + ) + )->add( + 'font', + array( + 'label' => $this->translate('Icons'), + 'title' => $this->translate('List all available icons'), + 'url' => 'doc/style/font' + ) + ); + } +} diff --git a/modules/doc/application/views/scripts/chapter.phtml b/modules/doc/application/views/scripts/chapter.phtml new file mode 100644 index 0000000..8cd4f6e --- /dev/null +++ b/modules/doc/application/views/scripts/chapter.phtml @@ -0,0 +1,6 @@ +<div class="controls"> + <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $this->tabs ?> +</div> +<div class="content chapter"> + <?= /** @var \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?> +</div> diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml new file mode 100644 index 0000000..9bf745a --- /dev/null +++ b/modules/doc/application/views/scripts/index/index.phtml @@ -0,0 +1,19 @@ +<div class="controls"> + <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?> +</div> +<div class="content"> + <ul> + <li><?= $this->qlink( + 'Icinga Web 2', + 'doc/icingaweb/toc', + null, + array('title' => $this->translate('Show the documentation\'s table of contents for Icinga Web 2')) + ) ?></li> + <li><?= $this->qlink( + $this->translate('Module documentations'), + 'doc/module/', + null, + array('title' => $this->translate('List all modules for which documentation is available')) + ) ?></li> + </ul> +</div> diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml new file mode 100644 index 0000000..f70d69a --- /dev/null +++ b/modules/doc/application/views/scripts/module/index.phtml @@ -0,0 +1,19 @@ +<div class="controls"> + <?= $this->tabs ?> +</div> + +<div class="content"> + <ul> + <?php foreach ($modules as $module): /** @var \Icinga\Application\Modules\Module $module */ ?> + <li><?= $this->qlink( + $module->getTitle(), + 'doc/module/toc', + array('moduleName' => $module->getName()), + array('title' => sprintf( + $this->translate('Show the documentation\'s table of contents for the %s'), + $module->getTitle() + )) + ) ?></li> + <?php endforeach ?> + </ul> +</div> diff --git a/modules/doc/application/views/scripts/pdf.phtml b/modules/doc/application/views/scripts/pdf.phtml new file mode 100644 index 0000000..2666efb --- /dev/null +++ b/modules/doc/application/views/scripts/pdf.phtml @@ -0,0 +1,5 @@ +<div class="content"> + <h1><?= /** @var string $title */ $title ?></h1> + <?= /** @var \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?> + <?= /** @var \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?> +</div> diff --git a/modules/doc/application/views/scripts/search/index.phtml b/modules/doc/application/views/scripts/search/index.phtml new file mode 100644 index 0000000..c613f04 --- /dev/null +++ b/modules/doc/application/views/scripts/search/index.phtml @@ -0,0 +1,8 @@ +<div class="content"> + <?php foreach (/** @var \Icinga\Module\Doc\Renderer\DocSearchRenderer[] $searches */ $searches as $title => $search): ?> + <h2><?= $this->escape($title) ?></h2> + <?= $search->isEmpty() + ? $this->translate('No documentation found matching the filter') + : $search ?> + <?php endforeach ?> +</div> diff --git a/modules/doc/application/views/scripts/style/font.phtml b/modules/doc/application/views/scripts/style/font.phtml new file mode 100644 index 0000000..c84a983 --- /dev/null +++ b/modules/doc/application/views/scripts/style/font.phtml @@ -0,0 +1,15 @@ +<div class="controls"> +<?= $this->tabs ?> +<h1>Icinga Web 2 Icons</h1> +</div> + +<div class="content"> +<?php foreach ($this->font->glyphs as $icon): ?> +<!-- TODO: move CSS away //--> +<div style="width: 33%; font-size: 1.5em; height: 2em; float: left;" class="<?= + $this->font->css_prefix_text . $icon->css +?>"> +<?= $this->escape($icon->css) ?> <span style="font-size: 0.6em">(0x<?= dechex($icon->code) ?>)</span> +</div> +<?php endforeach ?> +</div> diff --git a/modules/doc/application/views/scripts/style/guide.phtml b/modules/doc/application/views/scripts/style/guide.phtml new file mode 100644 index 0000000..f2f57d2 --- /dev/null +++ b/modules/doc/application/views/scripts/style/guide.phtml @@ -0,0 +1,112 @@ +<div class="controls"> + <?= $this->tabs ?> +</div> + +<div class="content styleguide"> + <div class="section"> + <h1>Icinga Web 2 Design Guidelines</h1> + + <ul class="toc"> + <li><a href="#headings">Headings</a></li> + <li><a href="#block-content">Block Content</a></li> + <li><a href="#tables">Tables</a></li> + <li><a href="#comment-list">Comment List</a></li> + <li><a href="#blockquote">Blockquote</a></li> + </ul> + </div> + + <div class="section"> + <h2 id="headings">Headings</h2> + <h1>Header h1</h1> + <h2>Header h2</h2> + <h3>Header h3</h3> + <h4>Header h4</h4> + <h5>Header h5</h5> + <h6>Header h6</h6> + </div> + + <div class="section"> + <h2 id="block-content">Block Content</h2> + <h3>Paragraph</h3> + <p> + This is a paragraph. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo + dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + A <a href="#">link inside a paragraph</a>. + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et + dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + </p> + </div> + + <div class="section"> + <h2 id="tables">Tables</h2> + <table class="common-table"> + <thead> + <tr> + <th>Table Head - th in thead</th> + <td>td in thead<td> + </tr> + </thead> + <tbody> + <tr> + <th>Tbody - th</th> + <td>Tbody - td</td> + </tr> + <tr> + <th>Tbody - th</th> + <td>Tbody - td</td> + </tr> + <tr> + <th>Tbody - th</th> + <td>Tbody - td</td> + </tr> + </tbody> + </table> + </div> + + <div class="section"> + <h2 id="comment-list"><?= $this->translate('Comment List') ?></h2> + <dl class="comment-list"> + <dt> + John Doe + <span class="comment-time"> + <?= $this->translate('commented') ?> + <span class="relative-time"><?= $this->translate('some time ago') ?></span> + </span> + <i class="remove-action icon-cancel"></i> + </dt> + <dd> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + <br> + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + </dd> + <dt> + Richard Roe + <span class="comment-time"> + <?= $this->translate('commented') ?> + <span class="relative-time"><?= $this->translate('some time ago') ?></span> + </span> + <i class="remove-action icon-cancel"></i> + </dt> + <dd> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore + <br> + et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + </dd> + </dl> + </div> + + <div class="section"> + <h2 id="blockquote"><?= $this->translate('Blockquote') ?></h2> + <blockquote> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. + At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore + magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. + Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. + </blockquote> + </div> +</div> diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml new file mode 100644 index 0000000..d08830b --- /dev/null +++ b/modules/doc/application/views/scripts/toc.phtml @@ -0,0 +1,6 @@ +<div class="controls"> + <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?> +</div> +<div class="content"> + <?= /** @var \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?> +</div> diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php new file mode 100644 index 0000000..8b909ef --- /dev/null +++ b/modules/doc/configuration.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +/** @var $this \Icinga\Application\Modules\Module */ + +$section = $this->menuSection(N_('Documentation'), array( + 'title' => 'Documentation', + 'icon' => 'book', + 'url' => 'doc', + 'priority' => 700 +)); + +$section->add('Icinga Web 2', array( + 'url' => 'doc/icingaweb/toc', +)); +$section->add('Module documentations', array( + 'url' => 'doc/module', +)); +$section->add(N_('Developer - Style'), array( + 'url' => 'doc/style/guide', + 'priority' => 790 +)); + +$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10); diff --git a/modules/doc/doc/01-About.md b/modules/doc/doc/01-About.md new file mode 100644 index 0000000..02e2cbf --- /dev/null +++ b/modules/doc/doc/01-About.md @@ -0,0 +1,6 @@ +# About the Doc Module <a id="doc-module-about"></a> + +Please read the following chapters for more insights on this module: + +* [Installation](02-Installation.md#doc-module-installation) +* [Module Documentation](03-Module-Documentation.md#module-documentation) diff --git a/modules/doc/doc/02-Installation.md b/modules/doc/doc/02-Installation.md new file mode 100644 index 0000000..6d93d42 --- /dev/null +++ b/modules/doc/doc/02-Installation.md @@ -0,0 +1,15 @@ +# Doc Module Installation <a id="doc-module-installation"></a> + +This module is provided with the Icinga Web 2 package and does +not need any extra installation step. + +## Enable the Module <a id="monitoring-module-enable"></a> + +Navigate to `Configuration` -> `Modules` -> `doc` and enable +the module. + +You can also enable the module during the setup wizard, or on the CLI: + +``` +icingacli module enable doc +``` diff --git a/modules/doc/doc/03-Module-Documentation.md b/modules/doc/doc/03-Module-Documentation.md new file mode 100644 index 0000000..5ce4a9a --- /dev/null +++ b/modules/doc/doc/03-Module-Documentation.md @@ -0,0 +1,87 @@ +# Writing Module Documentation <a id="module-documentation"></a> + +![Markdown](img/markdown.png) + +Icinga Web 2 is capable of viewing your module's documentation, if the documentation is written in +[Markdown](http://en.wikipedia.org/wiki/Markdown). Please refer to +[Markdown Syntax Documentation](http://daringfireball.net/projects/markdown/syntax) for Markdown's formatting syntax. + +## Where to Put Module Documentation? <a id="module-documentation-location"></a> + +By default, your module's Markdown documentation files must be placed in the `doc` directory beneath your module's root +directory, e.g.: + +``` +example-module/doc +``` + +## Chapters <a id="module-documentation-chapters"></a> + +Each Markdown documentation file represents a chapter of your module's documentation. The first found heading inside +each file is the chapter's title. The order of chapters is based on the case insensitive "Natural Order" of your files' +names. <dfn>Natural Order</dfn> means that the file names are ordered in the way which seems natural to humans. +It is best practice to prefix Markdown documentation file names with numbers to ensure that they appear in the correct +order, e.g.: + +``` +01-About.md +02-Installation.md +03-Configuration.md +``` + +## Table Of Contents <a id="module-documentation-toc"></a> + +The table of contents for your module's documentation is auto-generated based on all found headings inside each +Markdown documentation file. + +## Linking Between Headings <a id="module-documentation-linking"></a> + +For linking between headings, place an anchor **after the text** where you want to link to, e.g.: + +``` +# Heading <a id="heading"></a> Heading +``` + +Please note that anchors have to be unique across all your Markdown documentation files. + +Now you can reference the anchor either in the same or **in another** Markdown documentation file, e.g.: + +``` +This is a link to [Heading](#heading). +``` + +Other tools support linking between headings by giving the filename plus the anchor to link to, e.g.: + +``` +This is a link to [About/Heading](01-About.md#heading) +``` + +This syntax is also supported in Icinga Web 2. + +## Including Images <a id="module-documentation-images"></a> + +Images must placed in the `doc` directory beneath your module's root directory, e.g.: + +``` +/path/to/icingaweb2/modules/example-module/doc/img/example.png +``` + +Note that the `img` sub directory is not mandatory but good for organizing your directory structure. + +Module images can be accessed using the following URL: + +``` +{baseURL}/doc/module/{moduleName}/image/{image} e.g. icingaweb2/doc/module/example-module/image/img/example.png +``` + +Markdown's image syntax is very similar to Markdown's link syntax, but prefixed with an exclamation mark, e.g.: + +``` +![Alt text](http://path/to/img.png "Optional Title") +``` + +URLs to images inside your Markdown documentation files must be specified without the base URL, e.g.: + +``` +![Example](img/example.png) +``` diff --git a/modules/doc/doc/img/markdown.png b/modules/doc/doc/img/markdown.png Binary files differnew file mode 100644 index 0000000..93e729b --- /dev/null +++ b/modules/doc/doc/img/markdown.png diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php new file mode 100644 index 0000000..0caf3ad --- /dev/null +++ b/modules/doc/library/Doc/DocController.php @@ -0,0 +1,116 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use Icinga\Module\Doc\Renderer\DocSectionRenderer; +use Icinga\Module\Doc\Renderer\DocTocRenderer; +use Icinga\Web\Controller; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\OutputFormat; + +class DocController extends Controller +{ + /** + * {@inheritdoc} + */ + protected function moduleInit() + { + // Our UrlParams object does not take parameters from custom routes into account which is why we have to set + // them explicitly + if ($this->hasParam('chapter')) { + $this->params->set('chapter', $this->getParam('chapter')); + } + if ($this->hasParam('image')) { + $this->params->set('image', $this->getParam('image')); + } + if ($this->hasParam('moduleName')) { + $this->params->set('moduleName', $this->getParam('moduleName')); + } + } + + /** + * Render a chapter + * + * @param string $path Path to the documentation + * @param string $chapter ID of the chapter + * @param string $url URL to replace links with + * @param string $imageUrl URL to images + * @param array $urlParams Additional URL parameters + */ + protected function renderChapter($path, $chapter, $url, $imageUrl = null, array $urlParams = array()) + { + $parser = new DocParser($path); + $section = new DocSectionRenderer($parser->getDocTree(), DocSectionRenderer::decodeUrlParam($chapter)); + $this->view->section = $section + ->setHighlightSearch($this->params->get('highlight-search')) + ->setImageUrl($imageUrl) + ->setUrl($url) + ->setUrlParams($urlParams); + $first = null; + foreach ($section as $first) { + break; + } + $title = $first === null ? ucfirst($chapter) : $first->getTitle(); + $this->view->title = $title; + $this->getTabs() + ->add('toc', array( + 'active' => true, + 'title' => $title, + 'url' => Url::fromRequest() + )) + ->extend(new OutputFormat(array(OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON))); + $this->render('chapter', null, true); + } + + /** + * Render a toc + * + * @param string $path Path to the documentation + * @param string $name Name of the documentation + * @param string $url URL to replace links with + * @param array $urlParams Additional URL parameters + */ + protected function renderToc($path, $name, $url, array $urlParams = array()) + { + $parser = new DocParser($path); + $toc = new DocTocRenderer($parser->getDocTree()->getIterator()); + $this->view->toc = $toc + ->setUrl($url) + ->setUrlParams($urlParams); + $name = ucfirst($name); + $title = sprintf($this->translate('%s Documentation'), $name); + $this->getTabs() + ->add('toc', array( + 'active' => true, + 'title' => $title, + 'url' => Url::fromRequest() + )) + ->extend(new OutputFormat(array(OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON))); + $this->render('toc', null, true); + } + + /** + * Render a pdf + * + * @param string $path Path to the documentation + * @param string $name Name of the documentation + * @param string $url + * @param array $urlParams + */ + protected function renderPdf($path, $name, $url, array $urlParams = array()) + { + $parser = new DocParser($path); + $toc = new DocTocRenderer($parser->getDocTree()->getIterator()); + $this->view->toc = $toc + ->setUrl($url) + ->setUrlParams($urlParams); + $section = new DocSectionRenderer($parser->getDocTree()); + $this->view->section = $section + ->setUrl($url) + ->setUrlParams($urlParams); + $this->view->title = sprintf($this->translate('%s Documentation'), $name); + $this->_request->setParam('format', 'pdf'); + $this->_helper->viewRenderer->setRender('pdf', null, true); + } +} diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php new file mode 100644 index 0000000..7ddeaa9 --- /dev/null +++ b/modules/doc/library/Doc/DocParser.php @@ -0,0 +1,235 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use CachingIterator; +use RecursiveIteratorIterator; +use SplFileObject; +use SplStack; +use Icinga\Data\Tree\SimpleTree; +use Icinga\Exception\NotReadableError; +use Icinga\Util\DirectoryIterator; +use Icinga\Module\Doc\Exception\DocException; + +/** + * Parser for documentation written in Markdown + */ +class DocParser +{ + /** + * Internal identifier for Atx-style headers + * + * @var int + */ + const HEADER_ATX = 1; + + /** + * Internal identifier for Setext-style headers + * + * @var int + */ + const HEADER_SETEXT = 2; + + /** + * Path to the documentation + * + * @var string + */ + protected $path; + + /** + * Iterator over documentation files + * + * @var DirectoryIterator + */ + protected $docIterator; + + /** + * Create a new documentation parser for the given path + * + * @param string $path Path to the documentation + * + * @throws DocException If the documentation directory does not exist + * @throws NotReadableError If the documentation directory is not readable + */ + public function __construct($path) + { + if (! DirectoryIterator::isReadable($path)) { + throw new DocException( + mt('doc', 'Documentation directory \'%s\' is not readable'), + $path + ); + } + $this->path = $path; + $this->docIterator = new DirectoryIterator($path, 'md', DirectoryIterator::FILES_FIRST); + } + + /** + * Extract atx- or setext-style headers from the given lines + * + * @param string $line + * @param string $nextLine + * + * @return array|null An array containing the header and the header level or null if there's nothing to extract + */ + protected function extractHeader($line, $nextLine) + { + if (! $line) { + return null; + } + $header = null; + if ($line + && $line[0] === '#' + && preg_match('/^#+/', $line, $match) === 1 + ) { + // Atx + $level = strlen($match[0]); + $header = trim(substr($line, $level)); + if (! $header) { + return null; + } + $headerStyle = static::HEADER_ATX; + } elseif ($nextLine + && ($nextLine[0] === '=' || $nextLine[0] === '-') + && preg_match('/^[=-]+\s*$/', $nextLine, $match) === 1 + ) { + // Setext + $header = trim($line); + if (! $header) { + return null; + } + if ($match[0][0] === '=') { + $level = 1; + } else { + $level = 2; + } + $headerStyle = static::HEADER_SETEXT; + } + if ($header === null) { + return null; + } + if (strpos($header, '<') !== false + && preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match) + ) { + $header = str_replace($match[0], '', $header); + $id = $match['id']; + } else { + $id = null; + } + /** @noinspection PhpUndefinedVariableInspection */ + return array($header, $id, $level, $headerStyle); + } + + /** + * Generate unique section ID + * + * @param string $id + * @param string $filename + * @param SimpleTree $tree + * + * @return string + */ + protected function uuid($id, $filename, SimpleTree $tree) + { + $id = str_replace(' ', '-', $id); + if ($tree->getNode($id) === null) { + return $id; + } + $id = $id . '-' . md5($filename); + $offset = 0; + while ($tree->getNode($id)) { + if ($offset++ === 0) { + $id .= '-' . $offset; + } else { + $id = substr($id, 0, -1) . $offset; + } + } + return $id; + } + + /** + * Get the documentation tree + * + * @return SimpleTree + */ + public function getDocTree() + { + $tree = new SimpleTree(); + foreach (new RecursiveIteratorIterator($this->docIterator) as $filename) { + $file = new SplFileObject($filename); + $file->setFlags(SplFileObject::READ_AHEAD); + $stack = new SplStack(); + $cachingIterator = new CachingIterator($file); + $insideFencedCodeBlock = false; + + for ($cachingIterator->rewind(); $cachingIterator->valid(); $cachingIterator->next()) { + $line = $cachingIterator->current(); + $header = null; + + if (substr($line, 0, 3) === '```') { + $insideFencedCodeBlock = ! $insideFencedCodeBlock; + } elseif (! $insideFencedCodeBlock) { + $fileIterator = $cachingIterator->getInnerIterator(); + $header = $this->extractHeader($line, $fileIterator->valid() ? $fileIterator->current() : null); + } + + if ($header !== null) { + list($title, $id, $level, $headerStyle) = $header; + while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) { + $stack->pop(); + } + if ($id === null) { + $path = array(); + foreach ($stack as $section) { + /** @var $section DocSection */ + $path[] = $section->getTitle(); + } + $path[] = $title; + $id = implode('-', $path); + $noFollow = true; + } else { + $noFollow = false; + } + + $id = $this->uuid($id, $filename, $tree); + + $section = new DocSection(); + $section + ->setId($id) + ->setTitle($title) + ->setLevel($level) + ->setNoFollow($noFollow); + if ($stack->isEmpty()) { + $section->setChapter($section); + $tree->addChild($section); + } else { + $section->setChapter($stack->bottom()); + $tree->addChild($section, $stack->top()); + } + $stack->push($section); + if ($headerStyle === static::HEADER_SETEXT) { + $cachingIterator->next(); + continue; + } + } else { + if ($stack->isEmpty()) { + $title = ucfirst($file->getBasename('.' . pathinfo($file->getFilename(), PATHINFO_EXTENSION))); + $id = $this->uuid($title, $filename, $tree); + $section = new DocSection(); + $section + ->setId($id) + ->setTitle($title) + ->setLevel(1) + ->setNoFollow(true); + $section->setChapter($section); + $tree->addChild($section); + $stack->push($section); + } + $stack->top()->appendContent($line); + } + } + } + return $tree; + } +} diff --git a/modules/doc/library/Doc/DocSection.php b/modules/doc/library/Doc/DocSection.php new file mode 100644 index 0000000..ce5297e --- /dev/null +++ b/modules/doc/library/Doc/DocSection.php @@ -0,0 +1,159 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use Icinga\Data\Tree\TreeNode; + +/** + * A section of a documentation + */ +class DocSection extends TreeNode +{ + /** + * Chapter the section belongs to + * + * @var DocSection + */ + protected $chapter; + + /** + * Content of the section + * + * @var array + */ + protected $content = array(); + + /** + * Header level + * + * @var int + */ + protected $level; + + /** + * Whether to instruct search engines to not index the link to the section + * + * @var bool + */ + protected $noFollow; + + /** + * Title of the section + * + * @var string + */ + protected $title; + + /** + * Set the chapter the section belongs to + * + * @param DocSection $section + * + * @return $this + */ + public function setChapter(DocSection $section) + { + $this->chapter = $section; + return $this; + } + + /** + * Get the chapter the section belongs to + * + * @return DocSection + */ + public function getChapter() + { + return $this->chapter; + } + + /** + * Append content + * + * @param string $content + */ + public function appendContent($content) + { + $this->content[] = $content; + } + + /** + * Get the content of the section + * + * @return array + */ + public function getContent() + { + return $this->content; + } + + /** + * Set the header level + * + * @param int $level Header level + * + * @return $this + */ + public function setLevel($level) + { + $this->level = (int) $level; + return $this; + } + + /** + * Get the header level + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * Set whether to instruct search engines to not index the link to the section + * + * @param bool $noFollow Whether to instruct search engines to not index the link to the section + * + * @return $this + */ + public function setNoFollow($noFollow = true) + { + $this->noFollow = (bool) $noFollow; + return $this; + } + + /** + * Get whether to instruct search engines to not index the link to the section + * + * @return bool + */ + public function getNoFollow() + { + return $this->noFollow; + } + + /** + * Set the title of the section + * + * @param string $title Title of the section + * + * @return $this + */ + public function setTitle($title) + { + $this->title = (string) $title; + return $this; + } + + /** + * Get the title of the section + * + * @return string + */ + public function getTitle() + { + return $this->title; + } +} diff --git a/modules/doc/library/Doc/DocSectionFilterIterator.php b/modules/doc/library/Doc/DocSectionFilterIterator.php new file mode 100644 index 0000000..bac5a67 --- /dev/null +++ b/modules/doc/library/Doc/DocSectionFilterIterator.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use Countable; +use RecursiveFilterIterator; +use Icinga\Data\Tree\TreeNodeIterator; + +/** + * Recursive filter iterator over sections that are part of a particular chapter + * + * @method TreeNodeIterator getInnerIterator() { + * {@inheritdoc} + * } + */ +class DocSectionFilterIterator extends RecursiveFilterIterator implements Countable +{ + /** + * Chapter to filter for + * + * @var string + */ + protected $chapter; + + /** + * Create a new recursive filter iterator over sections that are part of a particular chapter + * + * @param TreeNodeIterator $iterator + * @param string $chapter The chapter to filter for + */ + public function __construct(TreeNodeIterator $iterator, $chapter) + { + parent::__construct($iterator); + $this->chapter = $chapter; + } + + /** + * Accept sections that are part of the given chapter + * + * @return bool Whether the current element of the iterator is acceptable + * through this filter + */ + public function accept(): bool + { + $section = $this->current(); + /** @var \Icinga\Module\Doc\DocSection $section */ + if ($section->getChapter()->getId() === $this->chapter) { + return true; + } + return false; + } + + public function getChildren(): self + { + return new static($this->getInnerIterator()->getChildren(), $this->chapter); + } + + public function count(): int + { + return iterator_count($this); + } + + /** + * Whether the filter swallowed every section + * + * @return bool + */ + public function isEmpty() + { + return $this->count() === 0; + } +} diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php new file mode 100644 index 0000000..7fa7807 --- /dev/null +++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Exception; + +/** + * Exception thrown if a chapter was not found + */ +class ChapterNotFoundException extends DocException +{ +} diff --git a/modules/doc/library/Doc/Exception/DocException.php b/modules/doc/library/Doc/Exception/DocException.php new file mode 100644 index 0000000..1d9e871 --- /dev/null +++ b/modules/doc/library/Doc/Exception/DocException.php @@ -0,0 +1,13 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Exception; + +use Icinga\Exception\IcingaException; + +/** + * Exception thrown if an error in the documentation module's library occurs + */ +class DocException extends IcingaException +{ +} diff --git a/modules/doc/library/Doc/Renderer/DocRenderer.php b/modules/doc/library/Doc/Renderer/DocRenderer.php new file mode 100644 index 0000000..cb1bc39 --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocRenderer.php @@ -0,0 +1,208 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +use Exception; +use Icinga\Exception\IcingaException; +use RecursiveIteratorIterator; +use Icinga\Application\Icinga; +use Icinga\Web\View; + +/** + * Base class for toc and section renderer + */ +abstract class DocRenderer extends RecursiveIteratorIterator +{ + /** + * URL to images + * + * @var string + */ + protected $imageUrl; + + /** + * URL to replace links with + * + * @var string + */ + protected $url; + + /** + * Additional URL parameters + * + * @var array + */ + protected $urlParams = array(); + + /** + * View + * + * @var View|null + */ + protected $view; + + /** + * Get the URL to images + * + * @return string + */ + public function getImageUrl() + { + return $this->imageUrl; + } + + /** + * Set the URL to images + * + * @param string $imageUrl + * + * @return $this + */ + public function setImageUrl($imageUrl) + { + $this->imageUrl = (string) $imageUrl; + return $this; + } + /** + * Get the URL to replace links with + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the URL to replace links with + * + * @param string $url + * + * @return $this + */ + public function setUrl($url) + { + $this->url = (string) $url; + return $this; + } + + /** + * Get additional URL parameters + * + * @return array + */ + public function getUrlParams() + { + return $this->urlParams; + } + + /** + * Set additional URL parameters + * + * @param array $urlParams + * + * @return $this + */ + public function setUrlParams(array $urlParams) + { + $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams); + return $this; + } + + /** + * Get the view + * + * @return View + */ + public function getView() + { + if ($this->view === null) { + $this->view = Icinga::app()->getViewRenderer()->view; + } + return $this->view; + } + + /** + * Set the view + * + * @param View $view + * + * @return $this + */ + public function setView(View $view) + { + $this->view = $view; + return $this; + } + + /** + * Encode an anchor identifier + * + * @param string $anchor + * + * @return string + */ + public static function encodeAnchor($anchor) + { + return rawurlencode($anchor); + } + + /** + * Decode an anchor identifier + * + * @param string $anchor + * + * @return string + */ + public static function decodeAnchor($anchor) + { + return rawurldecode($anchor); + } + + /** + * Encode a URL parameter + * + * @param string $param + * + * @return string + */ + public static function encodeUrlParam($param) + { + return str_replace(array('%2F','%5C'), array('%252F','%255C'), rawurlencode($param)); + } + + /** + * Decode a URL parameter + * + * @param string $param + * + * @return string + */ + public static function decodeUrlParam($param) + { + return str_replace(array('%2F', '%5C'), array('/', '\\'), $param); + } + + /** + * Render to HTML + * + * @return string + */ + abstract public function render(); + + /** + * Render to HTML + * + * @return string + * @see \Icinga\Module\Doc\Renderer::render() For the render method. + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return $e->getMessage() . ': ' . IcingaException::getConfidentialTraceAsString($e); + } + } +} diff --git a/modules/doc/library/Doc/Renderer/DocSearchRenderer.php b/modules/doc/library/Doc/Renderer/DocSearchRenderer.php new file mode 100644 index 0000000..c6e9ae2 --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocSearchRenderer.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +use RecursiveIteratorIterator; +use Icinga\Module\Doc\Search\DocSearchIterator; +use Icinga\Module\Doc\Search\DocSearchMatch; + +/** + * Renderer for doc searches + */ +class DocSearchRenderer extends DocRenderer +{ + /** + * The content to render + * + * @var array + */ + protected $content = array(); + + /** + * Create a new renderer for doc searches + * + * @param DocSearchIterator $iterator + */ + public function __construct(DocSearchIterator $iterator) + { + parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST); + } + + public function beginIteration(): void + { + $this->content[] = '<nav role="navigation"><ul class="toc">'; + } + + public function endIteration(): void + { + $this->content[] = '</ul></nav>'; + } + + public function beginChildren(): void + { + if ($this->getInnerIterator()->getMatches()) { + $this->content[] = '<ul class="toc">'; + } + } + + public function endChildren(): void + { + if ($this->getInnerIterator()->getMatches()) { + $this->content[] = '</ul>'; + } + } + + public function render() + { + foreach ($this as $section) { + if (($matches = $this->getInnerIterator()->getMatches()) === null) { + continue; + } + $title = $this->getView()->escape($section->getTitle()); + $contentMatches = array(); + foreach ($matches as $match) { + if ($match->getMatchType() === DocSearchMatch::MATCH_HEADER) { + $title = $match->highlight(); + } else { + $contentMatches[] = sprintf( + '<p>%s</p>', + $match->highlight() + ); + } + } + $path = $this->getView()->getHelper('Url')->url( + array_merge( + $this->getUrlParams(), + array( + 'chapter' => $this->encodeUrlParam($section->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->getView()->url( + $path, + array('highlight-search' => $this->getInnerIterator()->getSearch()->getInput()) + ); + /** @var \Icinga\Web\Url $url */ + $url->setAnchor($this->encodeAnchor($section->getId())); + $urlAttributes = array( + 'data-base-target' => '_next', + 'title' => $section->getId() === $section->getChapter()->getId() + ? sprintf( + $this->getView()->translate( + 'Show all matches of "%s" in the chapter "%s"', + 'search.render.section.link' + ), + $this->getInnerIterator()->getSearch()->getInput(), + $section->getChapter()->getTitle() + ) + : sprintf( + $this->getView()->translate( + 'Show all matches of "%s" in the section "%s" of the chapter "%s"', + 'search.render.section.link' + ), + $this->getInnerIterator()->getSearch()->getInput(), + $section->getTitle(), + $section->getChapter()->getTitle() + ) + ); + if ($section->getNoFollow()) { + $urlAttributes['rel'] = 'nofollow'; + } + $this->content[] = '<li>' . $this->getView()->qlink( + $title, + $url->getAbsoluteUrl(), + null, + $urlAttributes, + false + ); + if (! empty($contentMatches)) { + $this->content = array_merge($this->content, $contentMatches); + } + if (! $section->hasChildren()) { + $this->content[] = '</li>'; + } + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/library/Doc/Renderer/DocSectionRenderer.php b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php new file mode 100644 index 0000000..de2d4d4 --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php @@ -0,0 +1,346 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +require_once 'Parsedown/Parsedown.php'; + +use DOMDocument; +use DOMXPath; +use Parsedown; +use RecursiveIteratorIterator; +use Icinga\Data\Tree\SimpleTree; +use Icinga\Module\Doc\Exception\ChapterNotFoundException; +use Icinga\Module\Doc\DocSectionFilterIterator; +use Icinga\Module\Doc\Search\DocSearch; +use Icinga\Module\Doc\Search\DocSearchMatch; +use Icinga\Web\Dom\DomNodeIterator; +use Icinga\Web\Url; +use Icinga\Web\View; + +/** + * Section renderer + */ +class DocSectionRenderer extends DocRenderer +{ + /** + * Content to render + * + * @var array + */ + protected $content = array(); + + /** + * Search criteria to highlight + * + * @var string + */ + protected $highlightSearch; + + /** + * Parsedown instance + * + * @var Parsedown + */ + protected $parsedown; + + /** + * Documentation tree + * + * @var SimpleTree + */ + protected $tree; + + /** + * Create a new section renderer + * + * @param SimpleTree $tree The documentation tree + * @param string|null $chapter If not null, the chapter to filter for + * + * @throws ChapterNotFoundException If the chapter to filter for was not found + */ + public function __construct(SimpleTree $tree, $chapter = null) + { + if ($chapter !== null) { + $filter = new DocSectionFilterIterator($tree->getIterator(), $chapter); + if ($filter->isEmpty()) { + throw new ChapterNotFoundException( + mt('doc', 'Chapter %s not found'), + $chapter + ); + } + parent::__construct( + $filter, + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + parent::__construct($tree->getIterator(), RecursiveIteratorIterator::SELF_FIRST); + } + $this->tree = $tree; + $this->parsedown = Parsedown::instance(); + } + + /** + * Set the search criteria to highlight + * + * @param string $highlightSearch + * + * @return $this + */ + public function setHighlightSearch($highlightSearch) + { + $this->highlightSearch = $highlightSearch; + return $this; + } + + /** + * Get the search criteria to highlight + * + * @return string + */ + public function getHighlightSearch() + { + return $this->highlightSearch; + } + + /** + * Syntax highlighting for PHP code + * + * @param array $match + * + * @return string + */ + protected function highlightPhp($match) + { + return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>'; + } + + /** + * Highlight search criteria + * + * @param string $html + * @param DocSearch $search Search criteria + * + * @return string + */ + protected function highlightSearch($html, DocSearch $search) + { + $doc = new DOMDocument(); + @$doc->loadHTML($html); + $iter = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST); + foreach ($iter as $node) { + if ($node->nodeType !== XML_TEXT_NODE + || ($node->parentNode->nodeType === XML_ELEMENT_NODE && $node->parentNode->tagName === 'code') + ) { + continue; + } + $text = $node->nodeValue; + if (($match = $search->search($text)) === null) { + continue; + } + $matches = $match->getMatches(); + ksort($matches); + $offset = 0; + $fragment = $doc->createDocumentFragment(); + foreach ($matches as $position => $match) { + $fragment->appendChild($doc->createTextNode(substr($text, $offset, $position - $offset))); + $fragment->appendChild($doc->createElement('span', $match)) + ->setAttribute('class', DocSearchMatch::HIGHLIGHT_CSS_CLASS); + $offset = $position + strlen($match); + } + $fragment->appendChild($doc->createTextNode(substr($text, $offset))); + $node->parentNode->replaceChild($fragment, $node); + } + // Remove <!DOCTYPE + $doc->removeChild($doc->doctype); + // Remove <html><body> and </body></html> + return substr($doc->saveHTML(), 12, -15); + } + + /** + * Markup notes + * + * @param array $match + * + * @return string + */ + protected function markupNotes($match) + { + $doc = new DOMDocument(); + $doc->loadHTML($match[0]); + $xpath = new DOMXPath($doc); + $blockquote = $xpath->query('//blockquote[1]')->item(0); + /** @var \DOMElement $blockquote */ + if (strtolower(substr(trim($blockquote->nodeValue), 0, 5)) === 'note:') { + $blockquote->setAttribute('class', 'note'); + } + return $doc->saveXML($blockquote); + } + + /** + * Replace img src tags + * + * @param $match + * + * @return string + */ + protected function replaceImg($match) + { + $doc = new DOMDocument(); + $doc->loadHTML($match[0]); + $xpath = new DOMXPath($doc); + $img = $xpath->query('//img[1]')->item(0); + /** @var \DOMElement $img */ + $path = $this->getView()->getHelper('Url')->url( + array_merge( + array( + 'image' => trim($img->getAttribute('src')) + ), + $this->urlParams + ), + $this->imageUrl, + false, + false + ); + $url = $this->getView()->url($path); + /** @var \Icinga\Web\Url $url */ + $img->setAttribute('src', $url->getAbsoluteUrl()); + return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>' + } + + /** + * Replace chapter link + * + * @param array $match + * + * @return string + */ + protected function replaceChapterLink($match) + { + if (($chapter = $this->tree->getNode($this->decodeAnchor($match['chapter']))) === null) { + return $match[0]; + } + /** @var \Icinga\Module\Doc\DocSection $section */ + $path = $this->getView()->getHelper('Url')->url( + array_merge( + $this->urlParams, + array( + 'chapter' => $this->encodeUrlParam($chapter->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->getView()->url($path); + /** @var \Icinga\Web\Url $url */ + return sprintf( + '<a %s%shref="%s"', + strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '', + $chapter->getNoFollow() ? 'rel="nofollow" ' : '', + $url->getAbsoluteUrl() + ); + } + + /** + * Replace section link + * + * @param array $match + * + * @return string + */ + protected function replaceSectionLink($match) + { + if (($section = $this->tree->getNode($this->decodeAnchor($match['section']))) === null) { + return $match[0]; + } + /** @var \Icinga\Module\Doc\DocSection $section */ + $path = $this->getView()->getHelper('Url')->url( + array_merge( + $this->urlParams, + array( + 'chapter' => $this->encodeUrlParam($section->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->getView()->url($path); + /** @var \Icinga\Web\Url $url */ + $url->setAnchor($this->encodeAnchor($section->getId())); + return sprintf( + '<a %s%shref="%s"', + strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '', + $section->getNoFollow() ? 'rel="nofollow" ' : '', + $url->getAbsoluteUrl() + ); + } + + /** + * {@inheritdoc} + */ + public function render() + { + $search = null; + if (($highlightSearch = $this->getHighlightSearch()) !== null) { + $search = new DocSearch($highlightSearch); + } + foreach ($this as $section) { + $title = $section->getTitle(); + if ($search !== null && ($match = $search->search($title)) !== null) { + $title = $match->highlight(); + } else { + $title = $this->getView()->escape($title); + } + $number = ''; + for ($i = 0; $i < $this->getDepth() + 1; ++$i) { + if ($i > 0) { + $number .= '.'; + } + $number .= $this->getSubIterator($i)->key() + 1; + } + $this->content[] = sprintf( + '<a name="%1$s"></a><h%2$d>%3$s. %4$s</h%2$d>', + static::encodeAnchor($section->getId()), + $section->getLevel(), + $number, + $title + ); + $html = $this->parsedown->text(implode('', $section->getContent())); + if (empty($html)) { + continue; + } + $html = preg_replace_callback( + '#<pre><code class="language-php">(.*?)</code></pre>#s', + array($this, 'highlightPhp'), + $html + ); + $html = preg_replace_callback( + '/<img[^>]+>/', + array($this, 'replaceImg'), + $html + ); + $html = preg_replace_callback( + '#<blockquote>.+?</blockquote>#ms', + array($this, 'markupNotes'), + $html + ); + $html = preg_replace_callback( + '/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:(?!http:\/\/)[^"#]*)#(?P<section>[^"]+)"/', + array($this, 'replaceSectionLink'), + $html + ); + $html = preg_replace_callback( + '/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:\d+-)?(?P<chapter>[^\/"#]+).md"/', + array($this, 'replaceChapterLink'), + $html + ); + if ($search !== null) { + $html = $this->highlightSearch($html, $search); + } + $this->content[] = $html; + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/library/Doc/Renderer/DocTocRenderer.php b/modules/doc/library/Doc/Renderer/DocTocRenderer.php new file mode 100644 index 0000000..09e9a1d --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocTocRenderer.php @@ -0,0 +1,117 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +use Icinga\Data\Tree\TreeNodeIterator; +use RecursiveIteratorIterator; + +/** + * TOC renderer + */ +class DocTocRenderer extends DocRenderer +{ + /** + * CSS class for the HTML list element + * + * @var string + */ + const CSS_CLASS = 'toc'; + + /** + * Tag for the HTML list element + * + * @var string + */ + const HTML_LIST_TAG = 'ol'; + + /** + * Content to render + * + * @var array + */ + protected $content = array(); + + /** + * Create a new toc renderer + * + * @param TreeNodeIterator $iterator + */ + public function __construct(TreeNodeIterator $iterator) + { + parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST); + } + + public function beginIteration(): void + { + $this->content[] = sprintf('<nav role="navigation"><%s class="%s">', static::HTML_LIST_TAG, static::CSS_CLASS); + } + + public function endIteration(): void + { + $this->content[] = sprintf('</%s></nav>', static::HTML_LIST_TAG); + } + + public function beginChildren(): void + { + $this->content[] = sprintf('<%s class="%s">', static::HTML_LIST_TAG, static::CSS_CLASS); + } + + public function endChildren(): void + { + $this->content[] = sprintf('</%s>', static::HTML_LIST_TAG); + } + + public function render() + { + if ($this->getInnerIterator()->isEmpty()) { + return '<p>' . mt('doc', 'Documentation is empty.') . '</p>'; + } + $view = $this->getView(); + $zendUrlHelper = $view->getHelper('Url'); + foreach ($this as $section) { + $path = $zendUrlHelper->url( + array_merge( + $this->urlParams, + array( + 'chapter' => $this->encodeUrlParam($section->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $view->url($path); + /** @var \Icinga\Web\Url $url */ + if ($this->getDepth() > 0) { + $url->setAnchor($this->encodeAnchor($section->getId())); + } + $urlAttributes = array( + 'data-base-target' => '_next', + 'title' => $section->getId() === $section->getChapter()->getId() + ? sprintf( + $view->translate('Show the chapter "%s"', 'toc.render.section.link'), + $section->getChapter()->getTitle() + ) + : sprintf( + $view->translate('Show the section "%s" of the chapter "%s"', 'toc.render.section.link'), + $section->getTitle(), + $section->getChapter()->getTitle() + ) + ); + if ($section->getNoFollow()) { + $urlAttributes['rel'] = 'nofollow'; + } + $this->content[] = '<li>' . $this->getView()->qlink( + $section->getTitle(), + $url->getAbsoluteUrl(), + null, + $urlAttributes + ); + if (! $section->hasChildren()) { + $this->content[] = '</li>'; + } + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/library/Doc/Search/DocSearch.php b/modules/doc/library/Doc/Search/DocSearch.php new file mode 100644 index 0000000..20493e4 --- /dev/null +++ b/modules/doc/library/Doc/Search/DocSearch.php @@ -0,0 +1,95 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Search; + +/** + * Search documentation for a given search string + */ +class DocSearch +{ + /** + * Search string + * + * @var string + */ + protected $input; + + /** + * Search criteria + * + * @var array + */ + protected $search; + + /** + * Create a new doc search from the given search string + * + * @param string $search + */ + public function __construct($search) + { + $this->input = $search = (string) $search; + $criteria = array(); + if (preg_match_all('/"(?P<search>[^"]*)"/', $search, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $unquoted = array(); + $offset = 0; + foreach ($matches as $match) { + $fullMatch = $match[0]; + $searchMatch = $match['search']; + $unquoted[] = substr($search, $offset, $fullMatch[1] - $offset); + $offset = $fullMatch[1] + strlen($fullMatch[0]); + if (strlen($searchMatch[0]) > 0) { + $criteria[] = $searchMatch[0]; + } + } + $unquoted[] = substr($search, $offset); + $search = implode(' ', $unquoted); + } + $this->search = array_map( + 'strtolower', + array_unique(array_merge($criteria, array_filter(explode(' ', trim($search))))) + ); + } + + /** + * Get the search criteria + * + * @return array + */ + public function getCriteria() + { + return $this->search; + } + + /** + * Get the search string + * + * @return string + */ + public function getInput() + { + return $this->input; + } + + /** + * Search in the given line + * + * @param string $line + * + * @return DocSearchMatch|null + */ + public function search($line) + { + $match = new DocSearchMatch(); + $match->setLine($line); + foreach ($this->search as $criteria) { + $offset = 0; + while (($position = stripos($line, $criteria, $offset)) !== false) { + $match->appendMatch(substr($line, $position, strlen($criteria)), $position); + $offset = $position + 1; + } + } + return $match->isEmpty() ? null : $match; + } +} diff --git a/modules/doc/library/Doc/Search/DocSearchIterator.php b/modules/doc/library/Doc/Search/DocSearchIterator.php new file mode 100644 index 0000000..fd2c903 --- /dev/null +++ b/modules/doc/library/Doc/Search/DocSearchIterator.php @@ -0,0 +1,113 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Search; + +use RecursiveFilterIterator; +use RecursiveIteratorIterator; +use Icinga\Data\Tree\TreeNodeIterator; + +/** + * Iterator over doc sections that match a given search criteria + */ +class DocSearchIterator extends RecursiveFilterIterator +{ + /** + * Search criteria + * + * @var DocSearch + */ + protected $search; + + /** + * Current search matches + * + * @var DocSearchMatch[]|null + */ + protected $matches; + + /** + * Create a new iterator over doc sections that match the given search criteria + * + * @param TreeNodeIterator $iterator + * @param DocSearch $search + */ + public function __construct(TreeNodeIterator $iterator, DocSearch $search) + { + $this->search = $search; + parent::__construct($iterator); + } + + /** + * Accept sections that match the search + * + * @return bool Whether the current element of the iterator is acceptable + * through this filter + */ + public function accept(): bool + { + $section = $this->current(); + /** @var $section \Icinga\Module\Doc\DocSection */ + $matches = array(); + if (($match = $this->search->search($section->getTitle())) !== null) { + $matches[] = $match->setMatchType(DocSearchMatch::MATCH_HEADER); + } + foreach ($section->getContent() as $lineno => $line) { + if (($match = $this->search->search($line)) !== null) { + $matches[] = $match + ->setMatchType(DocSearchMatch::MATCH_CONTENT) + ->setLineno($lineno); + } + } + if (! empty($matches)) { + $this->matches = $matches; + return true; + } + if ($section->hasChildren()) { + $this->matches = null; + return true; + } + return false; + } + + /** + * Get the search criteria + * + * @return DocSearch + */ + public function getSearch() + { + return $this->search; + } + + public function getChildren(): self + { + return new static($this->getInnerIterator()->getChildren(), $this->search); + } + + /** + * Whether the search did not yield any match + * + * @return bool + */ + public function isEmpty() + { + $iter = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); + foreach ($iter as $section) { + if ($iter->getInnerIterator()->getMatches() !== null) { + return false; + } + } + return true; + } + + /** + * Get current matches + * + * @return DocSearchMatch[]|null + */ + public function getMatches() + { + return $this->matches; + } +} diff --git a/modules/doc/library/Doc/Search/DocSearchMatch.php b/modules/doc/library/Doc/Search/DocSearchMatch.php new file mode 100644 index 0000000..0f21748 --- /dev/null +++ b/modules/doc/library/Doc/Search/DocSearchMatch.php @@ -0,0 +1,215 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Search; + +use UnexpectedValueException; +use Icinga\Application\Icinga; +use Icinga\Web\View; + +/** + * A doc search match + */ +class DocSearchMatch +{ + /** + * CSS class for highlighting matches + * + * @var string + */ + const HIGHLIGHT_CSS_CLASS = 'search-highlight'; + + /** + * Header match + * + * @var int + */ + const MATCH_HEADER = 1; + + /** + * Content match + * + * @var int + */ + const MATCH_CONTENT = 2; + + /** + * Line + * + * @var string + */ + protected $line; + + /** + * Line number + * + * @var int + */ + protected $lineno; + + /** + * Type of the match + * + * @var int + */ + protected $matchType; + + /** + * Matches + * + * @var array + */ + protected $matches = array(); + + /** + * View + * + * @var View|null + */ + protected $view; + + /** + * Set the line + * + * @param string $line + * + * @return $this + */ + public function setLine($line) + { + $this->line = (string) $line; + return $this; + } + + /** + * Get the line + * + * @return string + */ + public function getLine() + { + return $this->line; + } + + /** + * Set the line number + * + * @param int $lineno + * + * @return $this + */ + public function setLineno($lineno) + { + $this->lineno = (int) $lineno; + return $this; + } + + /** + * Set the match type + * + * @param int $matchType + * + * @return $this + */ + public function setMatchType($matchType) + { + $matchType = (int) $matchType; + if ($matchType !== static::MATCH_HEADER && $matchType !== static::MATCH_CONTENT) { + throw new UnexpectedValueException(); + } + $this->matchType = $matchType; + return $this; + } + + /** + * Get the match type + * + * @return int + */ + public function getMatchType() + { + return $this->matchType; + } + + /** + * Append a match + * + * @param string $match + * @param int $position + * + * @return $this + */ + public function appendMatch($match, $position) + { + $this->matches[(int) $position] = (string) $match; + return $this; + } + + /** + * Get the matches + * + * @return array + */ + public function getMatches() + { + return $this->matches; + } + + /** + * Set the view + * + * @param View $view + * + * @return $this + */ + public function setView(View $view) + { + $this->view = $view; + return $this; + } + + /** + * Get the view + * + * @return View + */ + public function getView() + { + if ($this->view === null) { + $this->view = Icinga::app()->getViewRenderer()->view; + } + return $this->view; + } + + /** + * Get the line having matches highlighted + * + * @return string + */ + public function highlight() + { + $highlighted = ''; + $offset = 0; + $matches = $this->getMatches(); + ksort($matches); + foreach ($matches as $position => $match) { + $highlighted .= $this->getView()->escape(substr($this->line, $offset, $position - $offset)) + . '<span class="' . static::HIGHLIGHT_CSS_CLASS .'">' + . $this->getView()->escape($match) + . '</span>'; + $offset = $position + strlen($match); + } + $highlighted .= $this->getView()->escape(substr($this->line, $offset)); + return $highlighted; + } + + /** + * Whether the match is empty + * + * @return bool + */ + public function isEmpty() + { + return empty($this->matches); + } +} diff --git a/modules/doc/module.info b/modules/doc/module.info new file mode 100644 index 0000000..ac596db --- /dev/null +++ b/modules/doc/module.info @@ -0,0 +1,4 @@ +Module: doc +Version: 2.11.4 +Description: Documentation module + Extracts, shows and exports documentation for Icinga Web 2 and its modules. diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less new file mode 100644 index 0000000..007c5c5 --- /dev/null +++ b/modules/doc/public/css/module.less @@ -0,0 +1,109 @@ +/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +// Mixins + +.gradient(@a: @gray-lighter; @b: @gray-lightest) { + background: @a; + background: -webkit-gradient(linear, left top, left bottom, from(@a), to(@b)); + background: -webkit-linear-gradient(top, @a, @b); + background: -moz-linear-gradient(top, @a, @b); + background: -ms-linear-gradient(top, @a, @b); + background: -o-linear-gradient(top, @a, @b); + background: linear-gradient(to bottom, @a, @b); +} + +// General styles + +code { + color: @icinga-blue; + font-family: @font-family-fixed; +} + +pre > code { + color: inherit; +} + +.chapter a { + border-bottom: 1px dotted @gray-light; + font-weight: @font-weight-bold; + + &:hover { + border-bottom: 1px solid @text-color; + text-decoration: none; + } +} + +.content { + font-size: 1.167em; +} + +.search-highlight { + .rounded-corners(); + + background: @icinga-blue; + color: @text-color-on-icinga-blue; + padding: 0 0.3em 0 0.3em; +} + +.toc { + counter-reset: li; + list-style-type: none; + margin: 0; + padding: 0; + + li { + counter-increment: li; + margin-top: 0.25em; + + > .toc { + margin-left: 2em; + } + + a { + &:before { + color: @icinga-blue; + content: counters(li,".") " "; + display: inline-block; + font-size: small; + font-weight: @font-weight-bold; + min-width: 1.5em; + padding: 0.25em; + text-align: center; + } + + display: block; + } + } +} + +// Table styles + +table { + margin-bottom: 1em; + width: 100%; +} + +tbody > tr:nth-child(odd) { + .gradient() +} + +tbody > tr:nth-child(even) { + background: @body-bg-color; +} + +td, th { + padding: 0.5em; +} + +td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +th { + border-bottom: 2px solid @icinga-blue; + font-weight: @font-weight-bold; + text-align: left; + text-transform: uppercase; +} diff --git a/modules/doc/public/js/module.js b/modules/doc/public/js/module.js new file mode 100644 index 0000000..d5571ee --- /dev/null +++ b/modules/doc/public/js/module.js @@ -0,0 +1,30 @@ +/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +(function(Icinga) { + + var Doc = function(module) { + this.module = module; + this.initialize(); + this.module.icinga.logger.debug('Doc module loaded'); + }; + + Doc.prototype = { + + initialize: function() + { + this.module.on('rendered', this.rendered); + this.module.icinga.logger.debug('Doc module initialized'); + }, + + rendered: function(event) { + var $container = $(event.currentTarget); + if ($('> .content.styleguide', $container).length) { + $container.removeClass('module-doc'); + } + } + }; + + Icinga.availableModules.doc = Doc; + +}(Icinga)); + diff --git a/modules/doc/run.php b/modules/doc/run.php new file mode 100644 index 0000000..df9dd09 --- /dev/null +++ b/modules/doc/run.php @@ -0,0 +1,64 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +use Icinga\Application\Icinga; + +if (Icinga::app()->isCli()) { + return; +} + +$docModuleChapter = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/chapter/:chapter', + array( + 'controller' => 'module', + 'action' => 'chapter', + 'module' => 'doc' + ) +); + +$docIcingaWebChapter = new Zend_Controller_Router_Route( + 'doc/icingaweb/chapter/:chapter', + array( + 'controller' => 'icingaweb', + 'action' => 'chapter', + 'module' => 'doc' + ) +); + +$docModuleToc = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/toc', + array( + 'controller' => 'module', + 'action' => 'toc', + 'module' => 'doc' + ) +); + +$docModulePdf = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/pdf', + array( + 'controller' => 'module', + 'action' => 'pdf', + 'module' => 'doc' + ) +); + +$docModuleImg = new Zend_Controller_Router_Route_Regex( + 'doc/module/([^/]+)/image/(.+)', + array( + 'controller' => 'module', + 'action' => 'image', + 'module' => 'doc' + ), + array( + 'moduleName' => 1, + 'image' => 2 + ), + 'doc/module/%s/image/%s' +); + +$this->addRoute('doc/module/chapter', $docModuleChapter); +$this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter); +$this->addRoute('doc/module/toc', $docModuleToc); +$this->addRoute('doc/module/pdf', $docModulePdf); +$this->addRoute('doc/module/img', $docModuleImg); diff --git a/modules/migrate/application/clicommands/ConfigCommand.php b/modules/migrate/application/clicommands/ConfigCommand.php new file mode 100644 index 0000000..a5be144 --- /dev/null +++ b/modules/migrate/application/clicommands/ConfigCommand.php @@ -0,0 +1,119 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Migrate\Clicommands; + +use Icinga\Cli\Command; +use Icinga\Module\Migrate\Config\UserDomainMigration; +use Icinga\User; +use Icinga\Util\StringHelper; + +class ConfigCommand extends Command +{ + /** + * Rename users and user configurations according to a given domain + * + * The following configurations are taken into account: + * - Announcements + * - Preferences + * - Dashboards + * - Custom navigation items + * - Role configuration + * - Users and group memberships in database backends, if configured + * + * USAGE: + * + * icingacli migrate config users [options] + * + * OPTIONS: + * + * --to-domain=<to-domain> The new domain for the users + * + * --from-domain=<from-domain> Migrate only the users with the given domain. + * Use this switch in combination with --to-domain. + * + * --user=<user> Migrate only the given user in the format <user> or <user@domain> + * + * --map-file=<mapfile> File to use for renaming users + * + * --separator=<separator> Separator for the map file + * + * EXAMPLES: + * + * icingacli migrate config users ... + * + * Add the domain "icinga.com" to all users: + * + * --to-domain icinga.com + * + * Set the domain "example.com" on all users that have the domain "icinga.com": + * + * --to-domain example.com --from-domain icinga.com + * + * Set the domain "icinga.com" on the user "icingaadmin": + * + * --to-domain icinga.com --user icingaadmin + * + * Set the domain "icinga.com" on the users "icingaadmin@icinga.com" + * + * --to-domain example.com --user icingaadmin@icinga.com + * + * Rename users according to a map file: + * + * --map-file /path/to/mapfile --separator : + * + * MAPFILE: + * + * You may rename users according to a given map file. The map file must be separated by newlines. Each line then + * is specified in the format <from><separator><to>. The separator is specified with the --separator switch. + * + * Example content: + * + * icingaadmin:icingaadmin@icinga.com + * jdoe@example.com:jdoe@icinga.com + * rroe@icinga:rroe@icinga.com + */ + public function usersAction() + { + if ($this->params->has('map-file')) { + $mapFile = $this->params->get('map-file'); + $separator = $this->params->getRequired('separator'); + + $source = trim(file_get_contents($mapFile)); + $source = StringHelper::trimSplit($source, "\n"); + + $map = array(); + + array_walk($source, function ($item) use ($separator, &$map) { + list($from, $to) = StringHelper::trimSplit($item, $separator, 2); + $map[$from] = $to; + }); + + $migration = UserDomainMigration::fromMap($map); + } else { + $toDomain = $this->params->getRequired('to-domain'); + $fromDomain = $this->params->get('from-domain'); + $user = $this->params->get('user'); + + if ($user === null) { + $migration = UserDomainMigration::fromDomains($toDomain, $fromDomain); + } else { + if ($fromDomain !== null) { + $this->fail( + "Ambiguous arguments: Can't use --user in combination with --from-domain." + . " Please use the user@domain syntax for the --user switch instead." + ); + } + + $user = new User($user); + + $migrated = clone $user; + $migrated->setDomain($toDomain); + + $migration = UserDomainMigration::fromMap(array($user->getUsername() => $migrated->getUsername())); + } + } + + $migration->migrate(); + } +} diff --git a/modules/migrate/application/clicommands/NavigationCommand.php b/modules/migrate/application/clicommands/NavigationCommand.php new file mode 100644 index 0000000..06fb2a8 --- /dev/null +++ b/modules/migrate/application/clicommands/NavigationCommand.php @@ -0,0 +1,195 @@ +<?php + +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Migrate\Clicommands; + +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; +use Icinga\Cli\Command; +use Icinga\Data\ConfigObject; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use Icinga\Module\Icingadb\Compat\UrlMigrator; +use Icinga\Util\DirectoryIterator; +use Icinga\Web\Request; +use ipl\Web\Filter\QueryString; +use ipl\Web\Url; + +class NavigationCommand extends Command +{ + /** + * Migrate local user monitoring navigation items to the Icinga DB Web actions + * + * USAGE + * + * icingacli migrate navigation [options] + * + * OPTIONS: + * + * --user=<username> Migrate monitoring navigation items only for + * the given user. (Default *) + * + * --delete Remove the legacy files after successfully + * migrated the navigation items. + */ + public function indexAction() + { + $moduleManager = Icinga::app()->getModuleManager(); + if (! $moduleManager->hasEnabled('icingadb')) { + Logger::error('Icinga DB module is not enabled. Please verify that the module is installed and enabled.'); + return; + } + + $preferencesPath = Config::resolvePath('preferences'); + $sharedNavigation = Config::resolvePath('navigation'); + if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) { + Logger::info('There are no local user navigation items to migrate'); + return; + } + + $rc = 0; + $user = $this->params->get('user'); + $directories = new DirectoryIterator($preferencesPath); + + foreach ($directories as $directory) { + $username = $user; + if ($username !== null && $directories->key() !== $username) { + continue; + } + + if ($username === null) { + $username = $directories->key(); + } + + $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc); + + Logger::info('Migrating monitoring navigation items for user "%s" to the Icinga DB Web actions', $username); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems($hostActions, $directory . '/icingadb-host-actions.ini', $rc); + } + + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems($serviceActions, $directory . '/icingadb-service-actions.ini', $rc); + } + } + + // Start migrating shared navigation items + $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc); + + Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems($hostActions, $sharedNavigation . '/icingadb-host-actions.ini', $rc); + } + + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems($serviceActions, $sharedNavigation . '/icingadb-service-actions.ini', $rc); + } + + if ($rc > 0) { + Logger::error('Failed to migrate some monitoring navigation items'); + exit($rc); + } + + Logger::info('Successfully migrated all local user monitoring navigation items'); + } + + /** + * Migrate the given config to the given new config path + * + * @param Config $config + * @param string $path + * @param int $rc + */ + private function migrateNavigationItems($config, $path, &$rc) + { + $deleteLegacyFiles = $this->params->get('delete'); + $newConfig = $this->readFromIni($path, $rc); + $counter = 1; + + /** @var ConfigObject $configObject */ + foreach ($config->getConfigObject() as $configObject) { + // Change the config type from "host-action" to icingadb's new action + if (strpos($path, 'icingadb-host-actions') !== false) { + $configObject->type = 'icingadb-host-action'; + } else { + $configObject->type = 'icingadb-service-action'; + } + + $urlString = $configObject->get('url'); + if ($urlString !== null) { + $url = Url::fromPath($urlString, [], new Request()); + + try { + $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl(); + $configObject->url = rawurldecode($urlString); + } catch (\InvalidArgumentException $err) { + // Do nothing + } + } + + $legacyFilter = $configObject->get('filter'); + if ($legacyFilter !== null) { + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformFilter($filter); + if ($filter !== false) { + $configObject->filter = rawurldecode(QueryString::render($filter)); + } else { + unset($configObject->filter); + } + } + + $section = $config->key(); + while ($newConfig->hasSection($section)) { + $section = $config->key() . $counter++; + } + + $newConfig->setSection($section, $configObject); + } + + try { + if (! $newConfig->isEmpty()) { + $newConfig->saveIni(); + + // Remove the legacy file only if explicitly requested + if ($deleteLegacyFiles) { + unlink($config->getConfigFile()); + } + } + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + $rc = 256; + } + } + + /** + * Get the navigation items config from the given ini path + * + * @param string $path Absolute path of the ini file + * @param int $rc The return code used to exit the action + * + * @return Config + */ + private function readFromIni($path, &$rc) + { + try { + $config = Config::fromIni($path); + } catch (NotReadableError $error) { + if ($error->getPrevious() !== null) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + } else { + Logger::error($error->getMessage()); + } + + $config = new Config(); + $rc = 128; + } + + return $config; + } +} diff --git a/modules/migrate/application/clicommands/PreferencesCommand.php b/modules/migrate/application/clicommands/PreferencesCommand.php new file mode 100644 index 0000000..11d1edb --- /dev/null +++ b/modules/migrate/application/clicommands/PreferencesCommand.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Migrate\Clicommands; + +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Cli\Command; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use Icinga\File\Ini\IniParser; +use Icinga\User; +use Icinga\User\Preferences\PreferencesStore; +use Icinga\Util\DirectoryIterator; + +class PreferencesCommand extends Command +{ + /** + * Migrate local INI user preferences to a database + * + * USAGE + * + * icingacli migrate preferences [options] + * + * OPTIONS: + * + * --resource=<resource-name> The resource to use, if no current database config backend is configured. + * --no-set-config-backend Do not set the given resource as config backend automatically + */ + public function indexAction() + { + $resource = Config::app()->get('global', 'config_resource'); + if (empty($resource)) { + $resource = $this->params->getRequired('resource'); + } + + $resourceConfig = ResourceFactory::getResourceConfig($resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + $connection = ResourceFactory::createResource($resourceConfig); + + $preferencesPath = Config::resolvePath('preferences'); + if (! file_exists($preferencesPath)) { + Logger::info('There are no local user preferences to migrate'); + return; + } + + $rc = 0; + + $preferenceDirs = new DirectoryIterator($preferencesPath); + foreach ($preferenceDirs as $preferenceDir) { + if (! is_dir($preferenceDir)) { + continue; + } + + $userName = basename($preferenceDir); + + Logger::info('Migrating INI preferences for user "%s" to database...', $userName); + + $dbStore = new PreferencesStore(new ConfigObject(['connection' => $connection]), new User($userName)); + + try { + $dbStore->load(); + $dbStore->save( + new User\Preferences( + $this->loadIniFile($preferencesPath, (new User($userName))->getUsername()) + ) + ); + } catch (NotReadableError $e) { + if ($e->getPrevious() !== null) { + Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage()); + } else { + Logger::error($e->getMessage()); + } + + $rc = 128; + } catch (NotWritableError $e) { + Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage()); + $rc = 256; + } + } + + if ($rc > 0) { + Logger::error('Failed to migrate some user preferences'); + exit($rc); + } + + if ($this->params->has('resource') && ! $this->params->has('no-set-config-backend')) { + $appConfig = Config::app(); + $globalConfig = $appConfig->getSection('global'); + $globalConfig['config_resource'] = $resource; + + try { + $appConfig->saveIni(); + } catch (NotWritableError $e) { + Logger::error('Failed to update general configuration: %s', $e->getMessage()); + exit(256); + } + } + + Logger::info('Successfully migrated all local user preferences to database'); + } + + private function loadIniFile(string $filePath, string $username): array + { + $preferences = []; + $preferencesFile = sprintf( + '%s/%s/config.ini', + $filePath, + strtolower($username) + ); + + if (file_exists($preferencesFile)) { + if (! is_readable($preferencesFile)) { + throw new NotReadableError( + 'Preferences INI file %s for user %s is not readable', + $preferencesFile, + $username + ); + } else { + $preferences = IniParser::parseIniFile($preferencesFile)->toArray(); + } + } + + return $preferences; + } +} diff --git a/modules/migrate/library/Migrate/Config/UserDomainMigration.php b/modules/migrate/library/Migrate/Config/UserDomainMigration.php new file mode 100644 index 0000000..855a0ab --- /dev/null +++ b/modules/migrate/library/Migrate/Config/UserDomainMigration.php @@ -0,0 +1,378 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Migrate\Config; + +use Icinga\Application\Config; +use Icinga\Data\Db\DbConnection; +use Icinga\Data\Filter\Filter; +use Icinga\Data\ResourceFactory; +use Icinga\User; +use Icinga\Util\DirectoryIterator; +use Icinga\Util\StringHelper; +use Icinga\Web\Announcement\AnnouncementIniRepository; + +class UserDomainMigration +{ + protected $toDomain; + + protected $fromDomain; + + protected $map; + + public static function fromMap(array $map) + { + $static = new static(); + + $static->map = $map; + + return $static; + } + + public static function fromDomains($toDomain, $fromDomain = null) + { + $static = new static(); + + $static->toDomain = $toDomain; + $static->fromDomain = $fromDomain; + + return $static; + } + + protected function mustMigrate(User $user) + { + if ($user->getUsername() === '*') { + return false; + } + + if ($this->map !== null) { + return isset($this->map[$user->getUsername()]); + } + + if ($this->fromDomain !== null && $user->hasDomain() && $user->getDomain() !== $this->fromDomain) { + return false; + } + + return true; + } + + protected function migrateUser(User $user) + { + $migrated = clone $user; + + if ($this->map !== null) { + $migrated->setUsername($this->map[$user->getUsername()]); + } else { + $migrated->setDomain($this->toDomain); + } + + return $migrated; + } + + protected function migrateAnnounces() + { + $announces = new AnnouncementIniRepository(); + + $query = $announces->select(array('author')); + + if ($this->map !== null) { + $query->where('author', array_keys($this->map)); + } + + $migratedUsers = array(); + + foreach ($announces->select(array('author')) as $announce) { + $user = new User($announce->author); + + if (! $this->mustMigrate($user)) { + continue; + } + + if (isset($migratedUsers[$user->getUsername()])) { + continue; + } + + $migrated = $this->migrateUser($user); + + $announces->update( + 'announcement', + array('author' => $migrated->getUsername()), + Filter::where('author', $user->getUsername()) + ); + + $migratedUsers[$user->getUsername()] = true; + } + } + + protected function migrateDashboards() + { + $directory = Config::resolvePath('dashboards'); + + $migration = array(); + + if (DirectoryIterator::isReadable($directory)) { + foreach (new DirectoryIterator($directory) as $username => $path) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$path] = dirname($path) . '/' . $migrated->getUsername(); + } + + foreach ($migration as $from => $to) { + rename($from, $to); + } + } + } + + protected function migrateNavigation() + { + $directory = Config::resolvePath('navigation'); + + foreach (new DirectoryIterator($directory, 'ini') as $file) { + $config = Config::fromIni($file); + + foreach ($config as $navigation) { + $owner = $navigation->owner; + + if (! empty($owner)) { + $user = new User($owner); + + if ($this->mustMigrate($user)) { + $migrated = $this->migrateUser($user); + + $navigation->owner = $migrated->getUsername(); + } + } + + $users = $navigation->users; + + if (! empty($users)) { + $users = StringHelper::trimSplit($users); + + foreach ($users as &$username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $username = $migrated->getUsername(); + } + + $navigation->users = implode(',', $users); + } + } + + $config->saveIni(); + } + } + + protected function migratePreferences() + { + $config = Config::app(); + + $resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource')); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + /** @var DbConnection $conn */ + $conn = ResourceFactory::createResource($resourceConfig); + + $query = $conn + ->select() + ->from('icingaweb_user_preference', array('username')) + ->group('username'); + + if ($this->map !== null) { + $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map)))); + } + + $users = $query->fetchColumn(); + + $migration = array(); + + foreach ($users as $username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$username] = $migrated->getUsername(); + } + + if (! empty($migration)) { + $conn->getDbAdapter()->beginTransaction(); + + foreach ($migration as $originalUsername => $username) { + $conn->update( + 'icingaweb_user_preference', + array('username' => $username), + Filter::where('username', $originalUsername) + ); + } + + $conn->getDbAdapter()->commit(); + } + } + + protected function migrateRoles() + { + $roles = Config::app('roles'); + + foreach ($roles as $role) { + $users = $role->users; + + if (empty($users)) { + continue; + } + + $users = StringHelper::trimSplit($users); + + foreach ($users as &$username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $username = $migrated->getUsername(); + } + + $role->users = implode(',', $users); + } + + $roles->saveIni(); + } + + protected function migrateUsers() + { + foreach (Config::app('authentication') as $name => $config) { + if (strtolower($config->backend) !== 'db') { + continue; + } + + $resourceConfig = ResourceFactory::getResourceConfig($config->resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + /** @var DbConnection $conn */ + $conn = ResourceFactory::createResource($resourceConfig); + + $query = $conn + ->select() + ->from('icingaweb_user', array('name')) + ->group('name'); + + if ($this->map !== null) { + $query->applyFilter(Filter::matchAny(Filter::where('name', array_keys($this->map)))); + } + + $users = $query->fetchColumn(); + + $migration = array(); + + foreach ($users as $username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$username] = $migrated->getUsername(); + } + + if (! empty($migration)) { + $conn->getDbAdapter()->beginTransaction(); + + foreach ($migration as $originalUsername => $username) { + $conn->update( + 'icingaweb_user', + array('name' => $username), + Filter::where('name', $originalUsername) + ); + } + + $conn->getDbAdapter()->commit(); + } + } + + foreach (Config::app('groups') as $name => $config) { + if (strtolower($config->backend) !== 'db') { + continue; + } + + $resourceConfig = ResourceFactory::getResourceConfig($config->resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + /** @var DbConnection $conn */ + $conn = ResourceFactory::createResource($resourceConfig); + + $query = $conn + ->select() + ->from('icingaweb_group_membership', array('username')) + ->group('username'); + + if ($this->map !== null) { + $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map)))); + } + + $users = $query->fetchColumn(); + + $migration = array(); + + foreach ($users as $username) { + $user = new User($username); + + if (! $this->mustMigrate($user)) { + continue; + } + + $migrated = $this->migrateUser($user); + + $migration[$username] = $migrated->getUsername(); + } + + if (! empty($migration)) { + $conn->getDbAdapter()->beginTransaction(); + + foreach ($migration as $originalUsername => $username) { + $conn->update( + 'icingaweb_group_membership', + array('username' => $username), + Filter::where('username', $originalUsername) + ); + } + + $conn->getDbAdapter()->commit(); + } + } + } + + public function migrate() + { + $this->migrateAnnounces(); + $this->migrateDashboards(); + $this->migrateNavigation(); + $this->migratePreferences(); + $this->migrateRoles(); + $this->migrateUsers(); + } +} diff --git a/modules/migrate/module.info b/modules/migrate/module.info new file mode 100644 index 0000000..6eb2911 --- /dev/null +++ b/modules/migrate/module.info @@ -0,0 +1,5 @@ +Module: migrate +Version: 2.11.4 +Description: Migrate module + This module was introduced with the domain-aware authentication feature in version 2.5.0. + It helps you migrating users and user configurations according to a given domain. diff --git a/modules/monitoring/application/clicommands/ListCommand.php b/modules/monitoring/application/clicommands/ListCommand.php new file mode 100644 index 0000000..6dc4193 --- /dev/null +++ b/modules/monitoring/application/clicommands/ListCommand.php @@ -0,0 +1,400 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Clicommands; + +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Cli\CliUtils; +use Icinga\Date\DateFormatter; +use Icinga\Cli\Command; +use Icinga\File\Csv; +use Icinga\Module\Monitoring\Plugin\PerfdataSet; +use Exception; +use Icinga\Util\Json; + +/** + * Icinga monitoring objects + * + * This module is your interface to the Icinga monitoring application. + */ +class ListCommand extends Command +{ + protected $backend; + protected $dumpSql; + protected $defaultActionName = 'status'; + + public function init() + { + $this->backend = MonitoringBackend::instance($this->params->shift('backend')); + $this->dumpSql = $this->params->shift('showsql'); + } + + protected function getQuery($table, $columns) + { + $limit = $this->params->shift('limit'); + $format = $this->params->shift('format'); + if ($format !== null) { + if ($this->params->has('columns')) { + $columnParams = preg_split( + '/,/', + $this->params->shift('columns') + ); + $columns = array(); + foreach ($columnParams as $col) { + if (false !== ($pos = strpos($col, '='))) { + $columns[substr($col, 0, $pos)] = substr($col, $pos + 1); + } else { + $columns[] = $col; + } + } + } + } + + $query = $this->backend->select()->from($table, $columns); + if ($limit) { + $query->limit($limit, $this->params->shift('offset')); + } + foreach ($this->params->getParams() as $col => $filter) { + $query->where($col, $filter); + } + // $query->applyFilters($this->params->getParams()); + if ($this->dumpSql) { + echo wordwrap($query->dump(), 72); + exit; + } + + if ($format !== null) { + $this->showFormatted($query, $format, $columns); + } + + return $query; + } + + protected function showFormatted($query, $format, $columns) + { + $query = $query->getQuery(); + switch ($format) { + case 'json': + echo Json::sanitize($query->fetchAll()); + break; + case 'csv': + Csv::fromQuery($query)->dump(); + break; + default: + preg_match_all('~\$([a-z0-9_-]+)\$~', $format, $m); + $words = array(); + foreach ($columns as $key => $col) { + if (is_numeric($key)) { + if (in_array($col, $m[1])) { + $words[] = $col; + } + } else { + if (in_array($key, $m[1])) { + $words[] = $key; + } + } + } + foreach ($query->fetchAll() as $row) { + $output = $format; + foreach ($words as $word) { + $output = preg_replace( + '~\$' . $word . '\$~', + $row->{$word}, + $output + ); + } + echo $output . "\n"; + } + } + exit; + } + + /** + * List and filter hosts + * + * This command allows you to search and visualize your hosts in + * different ways. + * + * USAGE + * + * icingacli monitoring list hosts [options] + * + * OPTIONS + * + * --verbose Show detailled output + * --showsql Dump generated SQL query (DB backend only) + * + * --format=<csv|json|<custom>> + * Dump columns in the given format. <custom> format allows $column$ + * placeholders, e.g. --format='$host$: $service$'. This requires + * that the columns are specified within the --columns parameter. + * + * --<column>[=filter] + * Filter given column by optional filter. Boolean (1/0) columns are + * true if no filter value is given. + * + * --problems + * Only show unhandled problems (HARD state and not acknowledged/in downtime). + * + * --columns='<comma separated list of host/service columns>' + * Add a limited set of columns to the output. The following host + * attributes can be fetched: state, handled, output, acknowledged, in_downtime, perfdata last_state_change + * + * EXAMPLES + * + * icingacli monitoring list hosts --problems + * icingacli monitoring list hosts --problems --host_state_type 0 + * icingacli monitoring list hosts --host=local* + * icingacli monitoring list hosts --columns 'host,host_output' \ + * --format='$host$ ($host_output$)' + */ + public function hostsAction() + { + $columns = array( + 'host_name', + 'host_state', + 'host_output', + 'host_handled', + 'host_acknowledged', + 'host_in_downtime' + ); + $query = $this->getQuery('hoststatus', $columns) + ->order('host_name'); + echo $this->renderStatusQuery($query); + } + + /** + * List and filter services + * + * This command allows you to search and visualize your services in + * different ways. + * + * USAGE + * + * icingacli monitoring list services [options] + * + * OPTIONS + * + * --verbose Show detailled output + * --showsql Dump generated SQL query (DB backend only) + * + * --format=<csv|json|<custom>> + * Dump columns in the given format. <custom> format allows $column$ + * placeholders, e.g. --format='$host$: $service$'. This requires + * that the columns are specified within the --columns parameter. + * + * --<column>[=filter] + * Filter given column by optional filter. Boolean (1/0) columns are + * true if no filter value is given. + * + * --problems + * Only show unhandled problems (HARD state and not acknowledged/in downtime). + * + * --columns='<comma separated list of host/service columns>' + * Add a limited set of columns to the output. The following service + * attributes can be fetched: state, handled, output, acknowledged, in_downtime, perfdata last_state_change + * + * EXAMPLES + * + * icingacli monitoring list services --problems + * icingacli monitoring list services --problems --service_state_type 0 + * icingacli monitoring list services --host=local* --service=*disk* + * icingacli monitoring list services --columns 'host,service,service_output' \ + * --format='$host$: $service$ ($service_output$)' + */ + public function servicesAction() + { + $columns = array( + 'host_name', + 'host_state', + 'host_output', + 'host_handled', + 'host_acknowledged', + 'host_in_downtime', + 'service_description', + 'service_state', + 'service_acknowledged', + 'service_in_downtime', + 'service_handled', + 'service_output', + 'service_perfdata', + 'service_last_state_change' + ); + $query = $this->getQuery('servicestatus', $columns) + ->order('host_name'); + echo $this->renderStatusQuery($query); + } + + protected function renderStatusQuery($query) + { + $out = ''; + $last_host = null; + $screen = $this->screen; + $utils = new CliUtils($screen); + $maxCols = $screen->getColumns(); + $query = $query->getQuery(); + $rows = $query->fetchAll(); + $count = $query->count(); + $count = count($rows); + + for ($i = 0; $i < $count; $i++) { + $row = & $rows[$i]; + + $utils->setHostState($row->host_state); + if (! array_key_exists($i + 1, $rows) + || $row->host_name !== $rows[$i + 1]->host_name + ) { + $lastService = true; + } else { + $lastService = false; + } + + $hostUnhandled = ! ($row->host_state == 0 || $row->host_handled); + + if ($row->host_name !== $last_host) { + if (isset($row->service_description)) { + $out .= "\n"; + } + + $hostTxt = $utils->shortHostState(); + if ($hostUnhandled) { + $out .= $utils->hostStateBackground( + sprintf(' %s ', $utils->shortHostState()) + ); + } else { + $out .= sprintf( + '%s %s ', + $utils->hostStateBackground(' '), + $utils->shortHostState() + ); + } + $out .= sprintf( + " %s%s: %s\n", + $screen->underline($row->host_name), + $screen->colorize($utils->objectStateFlags('host', $row), 'lightblue'), + $row->host_output + ); + + if (isset($row->services_ok)) { + $out .= sprintf( + "%d services, %d problems (%d unhandled), %d OK\n", + $row->services_cnt, + $row->services_problem, + $row->services_problem_unhandled, + $row->services_ok + ); + } + } + + $last_host = $row->host_name; + if (! isset($row->service_description)) { + continue; + } + + $utils->setServiceState($row->service_state); + $serviceUnhandled = ! ( + $row->service_state == 0 || $row->service_handled + ); + + if ($lastService) { + $straight = ' '; + $leaf = '└'; + } else { + $straight = '│'; + $leaf = '├'; + } + $out .= $utils->hostStateBackground(' '); + + if ($serviceUnhandled) { + $out .= $utils->serviceStateBackground( + sprintf(' %s ', $utils->shortServiceState()) + ); + $emptyBg = ' '; + $emptySpace = ''; + } else { + $out .= sprintf( + '%s %s ', + $utils->serviceStateBackground(' '), + $utils->shortServiceState() + ); + $emptyBg = ' '; + $emptySpace = ' '; + } + + $emptyLine = "\n" + . $utils->hostStateBackground(' ') + . $utils->serviceStateBackground($emptyBg) + . $emptySpace + . ' ' . $straight . ' '; + + $perf = ''; + try { + $pset = PerfdataSet::fromString($row->service_perfdata); + $perfs = array(); + foreach ($pset as $p) { + if ($percent = $p->getPercentage()) { + if ($percent < 0 || $percent > 100) { + continue; + } + $perfs[] = ' ' + . $p->getLabel() + . ': ' + . $this->getPercentageSign($percent) + . ' ' + . number_format($percent, 2, ',', '.') + . '%'; + } + } + if (! empty($perfs)) { + $perf = ', ' . implode($perfs); + } + // TODO: fix wordwarp, then remove this line: + $perf = ''; + } catch (Exception $e) { + // Ignoring perfdata errors right now, we could show some hint + } + + $wrappedOutput = wordwrap( + preg_replace('~\@{3,}~', '@@@', $row->service_output), + $maxCols - 13 + ) . "\n"; + $out .= sprintf( + " %1s─ %s%s (%s)", + $leaf, + $screen->underline($row->service_description), + $screen->colorize($utils->objectStateFlags('service', $row) . $perf, 'lightblue'), + ucfirst(DateFormatter::timeSince($row->service_last_state_change)) + ); + if ($this->isVerbose) { + $out .= $emptyLine . preg_replace( + '/\n/', + $emptyLine, + $wrappedOutput + ) . "\n"; + } else { + $out .= "\n"; + } + } + + $out .= "\n"; + return $out; + } + + protected function getPercentageSign($percent) + { + $circles = array( + 0 => '○', + 15 => '◔', + 40 => '◑', + 65 => '◕', + 90 => '●', + ); + $last = $circles[0]; + foreach ($circles as $cur => $circle) { + if ($percent < $cur) { + return $last; + } + $last = $circle; + } + } +} diff --git a/modules/monitoring/application/clicommands/NrpeCommand.php b/modules/monitoring/application/clicommands/NrpeCommand.php new file mode 100644 index 0000000..fe82322 --- /dev/null +++ b/modules/monitoring/application/clicommands/NrpeCommand.php @@ -0,0 +1,58 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Clicommands; + +use Icinga\Protocol\Nrpe\Connection; +use Icinga\Cli\Command; +use Exception; + +/** + * NRPE + */ +class NrpeCommand extends Command +{ + protected $defaultActionName = 'check'; + + /** + * Execute an NRPE command + * + * This command will execute an NRPE check, fire it against the given host + * and also pass through all your parameters. Output will be shown, exit + * code respected. + * + * USAGE + * + * icingacli monitoring nrpe <host> <command> [--ssl] [nrpe options] + * + * EXAMPLE + * + * icingacli monitoring nrpe 127.0.0.1 CheckMEM --ssl --MaxWarn=80% \ + * --MaxCrit=90% --type=physical + */ + public function checkAction() + { + $host = $this->params->shift(); + if (! $host) { + echo $this->showUsage(); + exit(3); + } + $command = $this->params->shift(null, '_NRPE_CHECK'); + $port = $this->params->shift('port', 5666); + try { + $nrpe = new Connection($host, $port); + if ($this->params->shift('ssl')) { + $nrpe->useSsl(); + } + $args = array(); + foreach ($this->params->getParams() as $k => $v) { + $args[] = $k . '=' . $v; + } + echo $nrpe->sendCommand($command, $args) . "\n"; + exit($nrpe->getLastReturnCode()); + } catch (Exception $e) { + echo $e->getMessage() . "\n"; + exit(3); + } + } +} diff --git a/modules/monitoring/application/controllers/ActionsController.php b/modules/monitoring/application/controllers/ActionsController.php new file mode 100644 index 0000000..bc13e21 --- /dev/null +++ b/modules/monitoring/application/controllers/ActionsController.php @@ -0,0 +1,135 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimesCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm; +use Icinga\Module\Monitoring\Object\HostList; +use Icinga\Module\Monitoring\Object\ServiceList; + +/** + * Monitoring API + */ +class ActionsController extends Controller +{ + /** + * Get the filter from URL parameters or exit immediately if the filter is empty + * + * @return Filter + */ + protected function getFilterOrExitIfEmpty() + { + $filter = Filter::fromQueryString((string) $this->params); + if ($filter->isEmpty()) { + $this->getResponse()->json() + ->setFailData(array('filter' => 'Filter is required and must not be empty')) + ->sendResponse(); + } + return $filter; + } + + /** + * Schedule host downtimes + */ + public function scheduleHostDowntimeAction() + { + $filter = $this->getFilterOrExitIfEmpty(); + $hostList = new HostList($this->backend); + $hostList + ->applyFilter($this->getRestriction('monitoring/filter/objects')) + ->applyFilter($filter); + if (! $hostList->count()) { + $this->getResponse()->json() + ->setFailData(array('filter' => 'No hosts found matching the filter')) + ->sendResponse(); + } + $form = new ScheduleHostDowntimeCommandForm(); + $form + ->setIsApiTarget(true) + ->setBackend($this->backend) + ->setObjects($hostList->fetch()) + ->handleRequest($this->getRequest()); + } + + /** + * Remove host downtimes + */ + public function removeHostDowntimeAction() + { + $filter = $this->getFilterOrExitIfEmpty(); + $downtimes = $this->backend + ->select() + ->from('downtime', array('host_name', 'id' => 'downtime_internal_id', 'name' => 'downtime_name')) + ->where('object_type', 'host') + ->applyFilter($this->getRestriction('monitoring/filter/objects')) + ->applyFilter($filter); + if (! $downtimes->count()) { + $this->getResponse()->json() + ->setFailData(array('filter' => 'No downtimes found matching the filter')) + ->sendResponse(); + } + $form = new DeleteDowntimesCommandForm(); + $form + ->setIsApiTarget(true) + ->setDowntimes($downtimes->fetchAll()) + ->handleRequest($this->getRequest()); + // @TODO(el): Respond w/ the downtimes deleted instead of the notifiaction added by + // DeleteDowntimesCommandForm::onSuccess(). + } + + /** + * Schedule service downtimes + */ + public function scheduleServiceDowntimeAction() + { + $filter = $this->getFilterOrExitIfEmpty(); + $serviceList = new ServiceList($this->backend); + $serviceList + ->applyFilter($this->getRestriction('monitoring/filter/objects')) + ->applyFilter($filter); + if (! $serviceList->count()) { + $this->getResponse()->json() + ->setFailData(array('filter' => 'No services found matching the filter')) + ->sendResponse(); + } + $form = new ScheduleServiceDowntimeCommandForm(); + $form + ->setIsApiTarget(true) + ->setBackend($this->backend) + ->setObjects($serviceList->fetch()) + ->handleRequest($this->getRequest()); + } + + /** + * Remove service downtimes + */ + public function removeServiceDowntimeAction() + { + $filter = $this->getFilterOrExitIfEmpty(); + $downtimes = $this->backend + ->select() + ->from( + 'downtime', + array('host_name', 'service_description', 'id' => 'downtime_internal_id', 'name' => 'downtime_name') + ) + ->where('object_type', 'service') + ->applyFilter($this->getRestriction('monitoring/filter/objects')) + ->applyFilter($filter); + if (! $downtimes->count()) { + $this->getResponse()->json() + ->setFailData(array('filter' => 'No downtimes found matching the filter')) + ->sendResponse(); + } + $form = new DeleteDowntimesCommandForm(); + $form + ->setIsApiTarget(true) + ->setDowntimes($downtimes->fetchAll()) + ->handleRequest($this->getRequest()); + // @TODO(el): Respond w/ the downtimes deleted instead of the notifiaction added by + // DeleteDowntimesCommandForm::onSuccess(). + } +} diff --git a/modules/monitoring/application/controllers/CommentController.php b/modules/monitoring/application/controllers/CommentController.php new file mode 100644 index 0000000..e50473f --- /dev/null +++ b/modules/monitoring/application/controllers/CommentController.php @@ -0,0 +1,91 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Application\Hook; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +/** + * Display detailed information about a comment + */ +class CommentController extends Controller +{ + /** + * The fetched comment + * + * @var object + */ + protected $comment; + + /** + * Fetch the first comment with the given id and add tabs + */ + public function init() + { + $commentId = $this->params->getRequired('comment_id'); + + $query = $this->backend->select()->from('comment', array( + 'id' => 'comment_internal_id', + 'objecttype' => 'object_type', + 'comment' => 'comment_data', + 'author' => 'comment_author_name', + 'timestamp' => 'comment_timestamp', + 'type' => 'comment_type', + 'persistent' => 'comment_is_persistent', + 'expiration' => 'comment_expiration', + 'name' => 'comment_name', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + ))->where('comment_internal_id', $commentId); + $this->applyRestriction('monitoring/filter/objects', $query); + + if (false === $this->comment = $query->fetchRow()) { + $this->httpNotFound($this->translate('Comment not found')); + } + + $this->getTabs()->add( + 'comment', + array( + 'icon' => 'comment-empty', + 'label' => $this->translate('Comment'), + 'title' => $this->translate('Display detailed information about a comment.'), + 'url' =>'monitoring/comments/show' + ) + )->activate('comment')->extend(new DashboardAction())->extend(new MenuAction()); + + if (Hook::has('ticket')) { + $this->view->tickets = Hook::first('ticket'); + } + } + + /** + * Display comment detail view + */ + public function showAction() + { + $this->view->comment = $this->comment; + $this->view->title = $this->translate('Comments'); + + if ($this->hasPermission('monitoring/command/comment/delete')) { + $listUrl = Url::fromPath('monitoring/list/comments') + ->setQueryString('comment_type=comment|comment_type=ack'); + $form = new DeleteCommentCommandForm(); + $form + ->populate(array( + 'comment_id' => $this->comment->id, + 'comment_is_service' => isset($this->comment->service_description), + 'comment_name' => $this->comment->name, + 'redirect' => $listUrl + )) + ->handleRequest(); + $this->view->delCommentForm = $form; + } + } +} diff --git a/modules/monitoring/application/controllers/CommentsController.php b/modules/monitoring/application/controllers/CommentsController.php new file mode 100644 index 0000000..9de19a0 --- /dev/null +++ b/modules/monitoring/application/controllers/CommentsController.php @@ -0,0 +1,108 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentsCommandForm; +use Icinga\Web\Url; + +/** + * Display detailed information about comments + */ +class CommentsController extends Controller +{ + /** + * The comments view + * + * @var \Icinga\Module\Monitoring\DataView\Comment + */ + protected $comments; + + /** + * Filter from request + * + * @var Filter + */ + protected $filter; + + /** + * Fetch all comments matching the current filter and add tabs + */ + public function init() + { + $this->filter = Filter::fromQueryString(str_replace( + 'comment_id', + 'comment_internal_id', + (string) $this->params + )); + $query = $this->backend->select()->from('comment', array( + 'id' => 'comment_internal_id', + 'objecttype' => 'object_type', + 'comment' => 'comment_data', + 'author' => 'comment_author_name', + 'timestamp' => 'comment_timestamp', + 'type' => 'comment_type', + 'persistent' => 'comment_is_persistent', + 'expiration' => 'comment_expiration', + 'name' => 'comment_name', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + ))->addFilter($this->filter); + $this->applyRestriction('monitoring/filter/objects', $query); + + $this->comments = $query; + + $this->view->title = $this->translate('Comments'); + $this->getTabs()->add( + 'comments', + array( + 'icon' => 'comment-empty', + 'label' => $this->translate('Comments') . sprintf(' (%d)', $query->count()), + 'title' => $this->translate( + 'Display detailed information about multiple comments.' + ), + 'url' =>'monitoring/comments/show' + ) + )->activate('comments'); + } + + /** + * Display the detail view for a comment list + */ + public function showAction() + { + $this->view->comments = $this->comments; + $this->view->listAllLink = Url::fromPath('monitoring/list/comments') + ->setQueryString($this->filter->toQueryString()); + $this->view->removeAllLink = Url::fromPath('monitoring/comments/delete-all') + ->setParams($this->params); + } + + /** + * Display the form for removing a comment list + */ + public function deleteAllAction() + { + $this->assertPermission('monitoring/command/comment/delete'); + + $listCommentsLink = Url::fromPath('monitoring/list/comments') + ->setQueryString('comment_type=(comment|ack)'); + $delCommentForm = new DeleteCommentsCommandForm(); + $delCommentForm->setTitle($this->view->translate('Remove all Comments')); + $delCommentForm->addDescription(sprintf( + $this->translate('Confirm removal of %d comments.'), + $this->comments->count() + )); + $delCommentForm->setComments($this->comments->fetchAll()) + ->setRedirectUrl($listCommentsLink) + ->handleRequest(); + $this->view->delCommentForm = $delCommentForm; + $this->view->comments = $this->comments; + $this->view->listAllLink = Url::fromPath('monitoring/list/comments') + ->setQueryString($this->filter->toQueryString()); + } +} diff --git a/modules/monitoring/application/controllers/ConfigController.php b/modules/monitoring/application/controllers/ConfigController.php new file mode 100644 index 0000000..b8ca0a1 --- /dev/null +++ b/modules/monitoring/application/controllers/ConfigController.php @@ -0,0 +1,298 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Exception; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\ConfirmRemovalForm; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Forms\Config\TransportReorderForm; +use Icinga\Web\Controller; +use Icinga\Web\Notification; +use Icinga\Module\Monitoring\Forms\Config\BackendConfigForm; +use Icinga\Module\Monitoring\Forms\Config\SecurityConfigForm; +use Icinga\Module\Monitoring\Forms\Config\TransportConfigForm; + +/** + * Configuration controller for editing monitoring resources + */ +class ConfigController extends Controller +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->assertPermission('config/modules'); + $this->view->title = $this->translate('Backends'); + $this->view->defaultTitle = 'monitoring :: ' . $this->view->defaultTitle; + parent::init(); + } + + /** + * Display a list of available backends and command transports + */ + public function indexAction() + { + $this->view->commandTransportReorderForm = $form = new TransportReorderForm(); + $form->handleRequest(); + + $this->view->backendsConfig = $this->Config('backends'); + $this->view->tabs = $this->Module()->getConfigTabs()->activate('backends'); + } + + /** + * Edit a monitoring backend + */ + public function editbackendAction() + { + $backendName = $this->params->getRequired('backend-name'); + + $form = new BackendConfigForm(); + $form->setRedirectUrl('monitoring/config'); + $form->setTitle(sprintf($this->translate('Edit Monitoring Backend %s'), $backendName)); + $form->setIniConfig($this->Config('backends')); + $form->setResourceConfig(ResourceFactory::getResourceConfigs()); + $form->setOnSuccess(function (BackendConfigForm $form) use ($backendName) { + try { + $form->edit($backendName, array_map( + function ($v) { + return $v !== '' ? $v : null; + }, + $form->getValues() + )); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(sprintf(t('Monitoring backend "%s" successfully updated'), $backendName)); + return true; + } + + return false; + }); + + try { + $form->load($backendName); + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Monitoring backend "%s" not found'), $backendName)); + } + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Create a new monitoring backend + */ + public function createbackendAction() + { + $form = new BackendConfigForm(); + $form->setRedirectUrl('monitoring/config'); + $form->setTitle($this->translate('Create New Monitoring Backend')); + $form->setIniConfig($this->Config('backends')); + + try { + $form->setResourceConfig(ResourceFactory::getResourceConfigs()); + } catch (ConfigurationError $e) { + if ($this->hasPermission('config/resources')) { + Notification::error($e->getMessage()); + $this->redirectNow('config/createresource'); + } + + throw $e; // No permission for resource configuration, show the error + } + + $form->setOnSuccess(function (BackendConfigForm $form) { + try { + $form->add($form::transformEmptyValuesToNull($form->getValues())); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(t('Monitoring backend successfully created')); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Display a confirmation form to remove the backend identified by the 'backend' parameter + */ + public function removebackendAction() + { + $backendName = $this->params->getRequired('backend-name'); + + $backendForm = new BackendConfigForm(); + $backendForm->setIniConfig($this->Config('backends')); + $form = new ConfirmRemovalForm(); + $form->setRedirectUrl('monitoring/config'); + $form->setTitle(sprintf($this->translate('Remove Monitoring Backend %s'), $backendName)); + $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) { + try { + $backendForm->delete($backendName); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($backendForm->save()) { + Notification::success(sprintf(t('Monitoring backend "%s" successfully removed'), $backendName)); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Remove a command transport + */ + public function removetransportAction() + { + $transportName = $this->params->getRequired('transport'); + + $transportForm = new TransportConfigForm(); + $transportForm->setIniConfig($this->Config('commandtransports')); + $form = new ConfirmRemovalForm(); + $form->setRedirectUrl('monitoring/config'); + $form->setTitle(sprintf($this->translate('Remove Command Transport %s'), $transportName)); + $form->info( + $this->translate( + 'If you still have any environments or views referring to this transport, ' + . 'you won\'t be able to send commands anymore after deletion.' + ), + false + ); + $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($transportName, $transportForm) { + try { + $transportForm->delete($transportName); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($transportForm->save()) { + Notification::success(sprintf(t('Command transport "%s" successfully removed'), $transportName)); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Edit a command transport + */ + public function edittransportAction() + { + $transportName = $this->params->getRequired('transport'); + + $form = new TransportConfigForm(); + $form->setRedirectUrl('monitoring/config'); + $form->setTitle(sprintf($this->translate('Edit Command Transport %s'), $transportName)); + $form->setIniConfig($this->Config('commandtransports')); + $form->setInstanceNames( + MonitoringBackend::instance()->select()->from('instance', array('instance_name'))->fetchColumn() + ); + $form->setOnSuccess(function (TransportConfigForm $form) use ($transportName) { + try { + $form->edit($transportName, array_map( + function ($v) { + return $v !== '' ? $v : null; + }, + $form->getValues() + )); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(sprintf(t('Command transport "%s" successfully updated'), $transportName)); + return true; + } + + return false; + }); + + try { + $form->load($transportName); + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Command transport "%s" not found'), $transportName)); + } + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Create a new command transport + */ + public function createtransportAction() + { + $form = new TransportConfigForm(); + $form->setRedirectUrl('monitoring/config'); + $form->setTitle($this->translate('Create New Command Transport')); + $form->setIniConfig($this->Config('commandtransports')); + $form->setInstanceNames( + MonitoringBackend::instance()->select()->from('instance', array('instance_name'))->fetchColumn() + ); + $form->setOnSuccess(function (TransportConfigForm $form) { + try { + $form->add($form::transformEmptyValuesToNull($form->getValues())); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(t('Command transport successfully created')); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->form = $form; + $this->render('form'); + } + + /** + * Display a form to adjust security relevant settings + */ + public function securityAction() + { + $form = new SecurityConfigForm(); + $form->setIniConfig($this->Config()); + $form->handleRequest(); + + $this->view->form = $form; + $this->view->title = $this->translate('Security'); + $this->view->tabs = $this->Module()->getConfigTabs()->activate('security'); + } +} diff --git a/modules/monitoring/application/controllers/DowntimeController.php b/modules/monitoring/application/controllers/DowntimeController.php new file mode 100644 index 0000000..83c03dd --- /dev/null +++ b/modules/monitoring/application/controllers/DowntimeController.php @@ -0,0 +1,108 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Application\Hook; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm; +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +/** + * Display detailed information about a downtime + */ +class DowntimeController extends Controller +{ + /** + * The fetched downtime + * + * @var object + */ + protected $downtime; + + /** + * Fetch the downtime matching the given id and add tabs + */ + public function init() + { + $downtimeId = $this->params->getRequired('downtime_id'); + + $query = $this->backend->select()->from('downtime', array( + 'id' => 'downtime_internal_id', + 'objecttype' => 'object_type', + 'comment' => 'downtime_comment', + 'author_name' => 'downtime_author_name', + 'start' => 'downtime_start', + 'scheduled_start' => 'downtime_scheduled_start', + 'scheduled_end' => 'downtime_scheduled_end', + 'end' => 'downtime_end', + 'duration' => 'downtime_duration', + 'is_flexible' => 'downtime_is_flexible', + 'is_fixed' => 'downtime_is_fixed', + 'is_in_effect' => 'downtime_is_in_effect', + 'entry_time' => 'downtime_entry_time', + 'name' => 'downtime_name', + 'host_state', + 'service_state', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + ))->where('downtime_internal_id', $downtimeId); + $this->applyRestriction('monitoring/filter/objects', $query); + + if (false === $this->downtime = $query->fetchRow()) { + $this->httpNotFound($this->translate('Downtime not found')); + } + + $this->getTabs()->add( + 'downtime', + array( + + 'icon' => 'plug', + 'label' => $this->translate('Downtime'), + 'title' => $this->translate('Display detailed information about a downtime.'), + 'url' =>'monitoring/downtimes/show' + ) + )->activate('downtime')->extend(new DashboardAction())->extend(new MenuAction()); + + if (Hook::has('ticket')) { + $this->view->tickets = Hook::first('ticket'); + } + } + + /** + * Display the detail view for a downtime + */ + public function showAction() + { + $isService = isset($this->downtime->service_description); + $this->view->downtime = $this->downtime; + $this->view->isService = $isService; + $this->view->listAllLink = Url::fromPath('monitoring/list/downtimes'); + $this->view->showHostLink = Url::fromPath('monitoring/host/show')->setParam('host', $this->downtime->host_name); + $this->view->showServiceLink = Url::fromPath('monitoring/service/show') + ->setParam('host', $this->downtime->host_name) + ->setParam('service', $this->downtime->service_description); + $this->view->stateName = $isService ? Service::getStateText($this->downtime->service_state) + : Host::getStateText($this->downtime->host_state); + + $this->view->title = $this->translate('Downtimes'); + if ($this->hasPermission('monitoring/command/downtime/delete')) { + $form = new DeleteDowntimeCommandForm(); + $form + ->populate(array( + 'downtime_id' => $this->downtime->id, + 'downtime_is_service' => $isService, + 'downtime_name' => $this->downtime->name, + 'redirect' => Url::fromPath('monitoring/list/downtimes'), + )) + ->handleRequest(); + $this->view->delDowntimeForm = $form; + } + } +} diff --git a/modules/monitoring/application/controllers/DowntimesController.php b/modules/monitoring/application/controllers/DowntimesController.php new file mode 100644 index 0000000..4891203 --- /dev/null +++ b/modules/monitoring/application/controllers/DowntimesController.php @@ -0,0 +1,108 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimesCommandForm; +use Icinga\Web\Url; + +/** + * Display detailed information about downtimes + */ +class DowntimesController extends Controller +{ + /** + * The downtimes view + * + * @var \Icinga\Module\Monitoring\DataView\Downtime + */ + protected $downtimes; + + /** + * Filter from request + * + * @var Filter + */ + protected $filter; + + /** + * Fetch all downtimes matching the current filter and add tabs + */ + public function init() + { + $this->filter = Filter::fromQueryString(str_replace( + 'downtime_id', + 'downtime_internal_id', + (string) $this->params + )); + $query = $this->backend->select()->from('downtime', array( + 'id' => 'downtime_internal_id', + 'objecttype' => 'object_type', + 'comment' => 'downtime_comment', + 'author_name' => 'downtime_author_name', + 'start' => 'downtime_start', + 'scheduled_start' => 'downtime_scheduled_start', + 'scheduled_end' => 'downtime_scheduled_end', + 'end' => 'downtime_end', + 'duration' => 'downtime_duration', + 'is_flexible' => 'downtime_is_flexible', + 'is_fixed' => 'downtime_is_fixed', + 'is_in_effect' => 'downtime_is_in_effect', + 'entry_time' => 'downtime_entry_time', + 'name' => 'downtime_name', + 'host_state', + 'service_state', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + ))->addFilter($this->filter); + $this->applyRestriction('monitoring/filter/objects', $query); + + $this->downtimes = $query; + + $this->view->title = $this->translate('Downtimes'); + $this->getTabs()->add( + 'downtimes', + array( + 'icon' => 'plug', + 'label' => $this->translate('Downtimes') . sprintf(' (%d)', $query->count()), + 'title' => $this->translate('Display detailed information about multiple downtimes.'), + 'url' =>'monitoring/downtimes/show' + ) + )->activate('downtimes'); + } + + /** + * Display the detail view for a downtime list + */ + public function showAction() + { + $this->view->downtimes = $this->downtimes; + $this->view->listAllLink = Url::fromPath('monitoring/list/downtimes') + ->setQueryString($this->filter->toQueryString()); + $this->view->removeAllLink = Url::fromPath('monitoring/downtimes/delete-all')->setParams($this->params); + } + + /** + * Display the form for removing a downtime list + */ + public function deleteAllAction() + { + $this->assertPermission('monitoring/command/downtime/delete'); + $this->view->downtimes = $this->downtimes; + $this->view->listAllLink = Url::fromPath('monitoring/list/downtimes') + ->setQueryString($this->filter->toQueryString()); + $delDowntimeForm = new DeleteDowntimesCommandForm(); + $delDowntimeForm->setTitle($this->view->translate('Remove all Downtimes')); + $delDowntimeForm->addDescription(sprintf( + $this->translate('Confirm removal of %d downtimes.'), + $this->downtimes->count() + )); + $delDowntimeForm->setRedirectUrl(Url::fromPath('monitoring/list/downtimes')); + $delDowntimeForm->setDowntimes($this->downtimes->fetchAll())->handleRequest(); + $this->view->delAllDowntimeForm = $delDowntimeForm; + } +} diff --git a/modules/monitoring/application/controllers/EventController.php b/modules/monitoring/application/controllers/EventController.php new file mode 100644 index 0000000..08ab1bc --- /dev/null +++ b/modules/monitoring/application/controllers/EventController.php @@ -0,0 +1,551 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use DateTime; +use DateTimeZone; +use Icinga\Module\Monitoring\Hook\EventDetailsExtensionHook; +use Icinga\Application\Hook; +use InvalidArgumentException; +use Icinga\Data\Queryable; +use Icinga\Date\DateFormatter; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Util\TimezoneDetect; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; +use Icinga\Web\Widget\Tabextension\OutputFormat; + +class EventController extends Controller +{ + /** + * @var string[] + */ + protected $dataViewsByType = array( + 'notify' => 'notificationevent', + 'comment' => 'commentevent', + 'comment_deleted' => 'commentevent', + 'ack' => 'commentevent', + 'ack_deleted' => 'commentevent', + 'dt_comment' => 'commentevent', + 'dt_comment_deleted' => 'commentevent', + 'flapping' => 'flappingevent', + 'flapping_deleted' => 'flappingevent', + 'hard_state' => 'statechangeevent', + 'soft_state' => 'statechangeevent', + 'dt_start' => 'downtimeevent', + 'dt_end' => 'downtimeevent' + ); + + public function init() + { + if (Hook::has('ticket')) { + $this->view->tickets = Hook::first('ticket'); + } + } + + public function showAction() + { + $type = $this->params->shiftRequired('type'); + $id = $this->params->shiftRequired('id'); + + if (! isset($this->dataViewsByType[$type]) + || $this->applyRestriction( + 'monitoring/filter/objects', + $this->backend->select()->from('eventhistory', array('id'))->where('id', $id) + )->fetchRow() === false + ) { + $this->httpNotFound($this->translate('Event not found')); + } + + $event = $this->query($type, $id)->fetchRow(); + + if ($event === false) { + $this->httpNotFound($this->translate('Event not found')); + } + + $this->view->object = $object = $event->service_description === null + ? new Host($this->backend, $event->host_name) + : new Service($this->backend, $event->host_name, $event->service_description); + $object->fetch(); + + list($icon, $label) = $this->getIconAndLabel($type); + + $this->view->details = array_merge( + array(array($this->view->escape($this->translate('Type')), $label)), + $this->getDetails($type, $event) + ); + + $this->view->extensionsHtml = array(); + /** @var EventDetailsExtensionHook $hook */ + foreach (Hook::all('Monitoring\\EventDetailsExtension') as $hook) { + try { + $html = $hook->getHtmlForEvent($event); + } catch (\Exception $e) { + $html = $this->view->escape($e->getMessage()); + } + + if ($html) { + $module = $this->view->escape($hook->getModule()->getName()); + $this->view->extensionsHtml[] = + '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">' + . $html + . '</div>'; + } + } + + $this->view->title = $this->translate('Event Overview'); + $this->getTabs() + ->add('event', array( + 'title' => $label, + 'label' => $label, + 'url' => Url::fromRequest(), + 'active' => true + )) + ->extend(new OutputFormat()) + ->extend(new DashboardAction()) + ->extend(new MenuAction()); + } + + /** + * Return translated and escaped 'Yes' if the given condition is true, 'No' otherwise, 'N/A' if NULL + * + * @param bool|null $condition + * + * @return string + */ + protected function yesOrNo($condition) + { + if ($condition === null) { + return $this->view->escape($this->translate('N/A')); + } + + return $this->view->escape($condition ? $this->translate('Yes') : $this->translate('No')); + } + + /** + * Render the given duration in seconds as human readable HTML or 'N/A' if NULL + * + * @param int|null $seconds + * + * @return string + */ + protected function duration($seconds) + { + return $this->view->escape( + $seconds === null ? $this->translate('N/A') : DateFormatter::formatDuration($seconds) + ); + } + + /** + * Render the given percent number as human readable HTML or 'N/A' if NULL + * + * @param float|null $percent + * + * @return string + */ + protected function percent($percent) + { + return $this->view->escape( + $percent === null ? $this->translate('N/A') : sprintf($this->translate('%.2f%%'), $percent) + ); + } + + /** + * Render the given comment message as HTML or 'N/A' if NULL + * + * @param string|null $message + * + * @return string + */ + protected function comment($message) + { + return $this->view->nl2br($this->view->createTicketLinks($this->view->markdown($message))); + } + + /** + * Render a link to the given contact or 'N/A' if NULL + * + * @param string|null $name + * + * @return string + */ + protected function contact($name) + { + return $name === null + ? $this->view->escape($this->translate('N/A')) + : $this->view->qlink($name, Url::fromPath('monitoring/show/contact', array('contact_name' => $name))); + } + + /** + * Render the given monitored object state as human readable HTML or 'N/A' if NULL + * + * @param bool $isService + * @param int|null $state + * + * @return string + */ + protected function state($isService, $state) + { + if ($state === null) { + return $this->view->escape($this->translate('N/A')); + } + + try { + $stateText = $isService + ? Service::getStateText($state, true) + : Host::getStateText($state, true); + } catch (InvalidArgumentException $e) { + return $this->view->escape($this->translate('N/A')); + } + + return '<span class="badge state-' . ($isService ? Service::getStateText($state) : Host::getStateText($state)) + . '"> </span><span class="state-label">' . $this->view->escape($stateText) . '</span>'; + } + + /** + * Render the given plugin output as human readable HTML + * + * @param string $output + * + * @return string + */ + protected function pluginOutput($output) + { + return $this->view->getHelper('PluginOutput')->pluginOutput($output); + } + + /** + * Return the icon and the label for the given event type + * + * @param string $eventType + * + * @return string[] + */ + protected function getIconAndLabel($eventType) + { + switch ($eventType) { + case 'notify': + return array('bell', $this->translate('Notification', 'tooltip')); + case 'comment': + return array('comment-empty', $this->translate('Comment', 'tooltip')); + case 'comment_deleted': + return array('cancel', $this->translate('Comment removed', 'tooltip')); + case 'ack': + return array('ok', $this->translate('Acknowledged', 'tooltip')); + case 'ack_deleted': + return array('ok', $this->translate('Acknowledgement removed', 'tooltip')); + case 'dt_comment': + return array('plug', $this->translate('Downtime scheduled', 'tooltip')); + case 'dt_comment_deleted': + return array('plug', $this->translate('Downtime removed', 'tooltip')); + case 'flapping': + return array('flapping', $this->translate('Flapping started', 'tooltip')); + case 'flapping_deleted': + return array('flapping', $this->translate('Flapping stopped', 'tooltip')); + case 'hard_state': + return array('warning-empty', $this->translate('Hard state change')); + case 'soft_state': + return array('spinner', $this->translate('Soft state change')); + case 'dt_start': + return array('plug', $this->translate('Downtime started', 'tooltip')); + case 'dt_end': + return array('plug', $this->translate('Downtime ended', 'tooltip')); + } + } + + /** + * Return a query for the given event ID of the given type + * + * @param string $type + * @param int $id + * + * @return Queryable + */ + protected function query($type, $id) + { + switch ($this->dataViewsByType[$type]) { + case 'downtimeevent': + return $this->backend->select() + ->from('downtimeevent', array( + 'entry_time' => 'downtimeevent_entry_time', + 'author_name' => 'downtimeevent_author_name', + 'comment_data' => 'downtimeevent_comment_data', + 'is_fixed' => 'downtimeevent_is_fixed', + 'scheduled_start_time' => 'downtimeevent_scheduled_start_time', + 'scheduled_end_time' => 'downtimeevent_scheduled_end_time', + 'was_started' => 'downtimeevent_was_started', + 'actual_start_time' => 'downtimeevent_actual_start_time', + 'actual_end_time' => 'downtimeevent_actual_end_time', + 'was_cancelled' => 'downtimeevent_was_cancelled', + 'is_in_effect' => 'downtimeevent_is_in_effect', + 'trigger_time' => 'downtimeevent_trigger_time', + 'host_name', + 'service_description' + )) + ->where('downtimeevent_id', $id); + case 'commentevent': + return $this->backend->select() + ->from('commentevent', array( + 'entry_type' => 'commentevent_entry_type', + 'comment_time' => 'commentevent_comment_time', + 'author_name' => 'commentevent_author_name', + 'comment_data' => 'commentevent_comment_data', + 'is_persistent' => 'commentevent_is_persistent', + 'comment_source' => 'commentevent_comment_source', + 'expires' => 'commentevent_expires', + 'expiration_time' => 'commentevent_expiration_time', + 'deletion_time' => 'commentevent_deletion_time', + 'host_name', + 'service_description' + )) + ->where('commentevent_id', $id); + case 'flappingevent': + return $this->backend->select() + ->from('flappingevent', array( + 'event_time' => 'flappingevent_event_time', + 'reason_type' => 'flappingevent_reason_type', + 'percent_state_change' => 'flappingevent_percent_state_change', + 'low_threshold' => 'flappingevent_low_threshold', + 'high_threshold' => 'flappingevent_high_threshold', + 'host_name', + 'service_description' + )) + ->where('flappingevent_id', $id) + ->where('flappingevent_event_type', $type); + case 'notificationevent': + return $this->backend->select() + ->from('notificationevent', array( + 'notification_reason' => 'notificationevent_reason', + 'start_time' => 'notificationevent_start_time', + 'end_time' => 'notificationevent_end_time', + 'state' => 'notificationevent_state', + 'output' => 'notificationevent_output', + 'long_output' => 'notificationevent_long_output', + 'escalated' => 'notificationevent_escalated', + 'contacts_notified' => 'notificationevent_contacts_notified', + 'host_name', + 'service_description' + )) + ->where('notificationevent_id', $id); + case 'statechangeevent': + return $this->backend->select() + ->from('statechangeevent', array( + 'state_time' => 'statechangeevent_state_time', + 'state' => 'statechangeevent_state', + 'current_check_attempt' => 'statechangeevent_current_check_attempt', + 'max_check_attempts' => 'statechangeevent_max_check_attempts', + 'last_state' => 'statechangeevent_last_state', + 'last_hard_state' => 'statechangeevent_last_hard_state', + 'output' => 'statechangeevent_output', + 'long_output' => 'statechangeevent_long_output', + 'check_source' => 'statechangeevent_check_source', + 'host_name', + 'service_description' + )) + ->where('statechangeevent_id', $id) + ->where('statechangeevent_state_change', 1) + ->where('statechangeevent_state_type', $type); + } + } + + /** + * Return the given event's data prepared for a name-value table + * + * @param string $type + * @param \stdClass $event + * + * @return string[][] + */ + protected function getDetails($type, $event) + { + switch ($type) { + case 'dt_start': + case 'dt_end': + $details = array(array( + array($this->translate('Entry time'), DateFormatter::formatDateTime($event->entry_time)), + array($this->translate('Is fixed'), $this->yesOrNo($event->is_fixed)), + array($this->translate('Is in effect'), $this->yesOrNo($event->is_in_effect)), + array($this->translate('Was started'), $this->yesOrNo($event->was_started)) + )); + + if ($type === 'dt_end') { + $details[] = array( + array($this->translate('Was cancelled'), $this->yesOrNo($event->was_cancelled)) + ); + } + + $details[] = array( + array($this->translate('Trigger time'), DateFormatter::formatDateTime($event->trigger_time)), + array( + $this->translate('Scheduled start time'), + DateFormatter::formatDateTime($event->scheduled_start_time) + ), + array( + $this->translate('Actual start time'), + DateFormatter::formatDateTime($event->actual_start_time) + ), + array( + $this->translate('Scheduled end time'), + DateFormatter::formatDateTime($event->scheduled_end_time) + ) + ); + + if ($type === 'dt_end') { + $details[] = array( + array( + $this->translate('Actual end time'), + DateFormatter::formatDateTime($event->actual_end_time) + ) + ); + } + + $details[] = array( + array($this->translate('Author'), $this->contact($event->author_name)), + array($this->translate('Comment'), $this->comment($event->comment_data)) + ); + + return call_user_func_array('array_merge', $details); + case 'comment': + case 'comment_deleted': + case 'ack': + case 'ack_deleted': + case 'dt_comment': + case 'dt_comment_deleted': + switch ($event->entry_type) { + case 'comment': + $entryType = $this->translate('User comment'); + break; + case 'downtime': + $entryType = $this->translate('Scheduled downtime'); + break; + case 'flapping': + $entryType = $this->translate('Flapping'); + break; + case 'ack': + $entryType = $this->translate('Acknowledgement'); + break; + default: + $entryType = $this->translate('N/A'); + } + + switch ($event->comment_source) { + case 'icinga': + $commentSource = $this->translate('Icinga'); + break; + case 'user': + $commentSource = $this->translate('User'); + break; + default: + $commentSource = $this->translate('N/A'); + } + + return array( + array($this->translate('Time'), DateFormatter::formatDateTime($event->comment_time)), + array($this->translate('Source'), $this->view->escape($commentSource)), + array($this->translate('Entry type'), $this->view->escape($entryType)), + array($this->translate('Author'), $this->contact($event->author_name)), + array($this->translate('Is persistent'), $this->yesOrNo($event->is_persistent)), + array($this->translate('Expires'), $this->yesOrNo($event->expires)), + array($this->translate('Expiration time'), DateFormatter::formatDateTime($event->expiration_time)), + array($this->translate('Deletion time'), DateFormatter::formatDateTime($event->deletion_time)), + array($this->translate('Message'), $this->comment($event->comment_data)) + ); + case 'flapping': + case 'flapping_deleted': + switch ($event->reason_type) { + case 'stopped': + $reasonType = $this->translate('Flapping stopped normally'); + break; + case 'disabled': + $reasonType = $this->translate('Flapping was disabled'); + break; + default: + $reasonType = $this->translate('N/A'); + } + + return array( + array($this->translate('Event time'), DateFormatter::formatDateTime($event->event_time)), + array($this->translate('Reason'), $this->view->escape($reasonType)), + array($this->translate('State change'), $this->percent($event->percent_state_change)), + array($this->translate('Low threshold'), $this->percent($event->low_threshold)), + array($this->translate('High threshold'), $this->percent($event->high_threshold)) + ); + case 'notify': + switch ($event->notification_reason) { + case 'normal_notification': + $notificationReason = $this->translate('Normal notification'); + break; + case 'ack': + $notificationReason = $this->translate('Problem acknowledgement'); + break; + case 'flapping_started': + $notificationReason = $this->translate('Flapping started'); + break; + case 'flapping_stopped': + $notificationReason = $this->translate('Flapping stopped'); + break; + case 'flapping_disabled': + $notificationReason = $this->translate('Flapping was disabled'); + break; + case 'dt_start': + $notificationReason = $this->translate('Downtime started'); + break; + case 'dt_end': + $notificationReason = $this->translate('Downtime ended'); + break; + case 'dt_cancel': + $notificationReason = $this->translate('Downtime was cancelled'); + break; + case 'custom_notification': + $notificationReason = $this->translate('Custom notification'); + break; + default: + $notificationReason = $this->translate('N/A'); + } + + $details = array( + array($this->translate('Start time'), DateFormatter::formatDateTime($event->start_time)), + array($this->translate('End time'), DateFormatter::formatDateTime($event->end_time)), + array($this->translate('Reason'), $this->view->escape($notificationReason)), + array( + $this->translate('State'), + $this->state($event->service_description !== null, $event->state) + ), + array($this->translate('Escalated'), $this->yesOrNo($event->escalated)), + array($this->translate('Contacts notified'), (int) $event->contacts_notified), + array( + $this->translate('Output'), + $this->pluginOutput($event->output) . $this->pluginOutput($event->long_output) + ) + ); + + return $details; + case 'hard_state': + case 'soft_state': + $isService = $event->service_description !== null; + + $details = array( + array($this->translate('State time'), DateFormatter::formatDateTime($event->state_time)), + array($this->translate('State'), $this->state($isService, $event->state)), + array($this->translate('Check source'), $event->check_source), + array($this->translate('Check attempt'), $this->view->escape(sprintf( + $this->translate('%d of %d'), + (int) $event->current_check_attempt, + (int) $event->max_check_attempts + ))), + array($this->translate('Last state'), $this->state($isService, $event->last_state)), + array($this->translate('Last hard state'), $this->state($isService, $event->last_hard_state)), + array( + $this->translate('Output'), + $this->pluginOutput($event->output) . $this->pluginOutput($event->long_output) + ) + ); + + return $details; + } + } +} diff --git a/modules/monitoring/application/controllers/HealthController.php b/modules/monitoring/application/controllers/HealthController.php new file mode 100644 index 0000000..48dd580 --- /dev/null +++ b/modules/monitoring/application/controllers/HealthController.php @@ -0,0 +1,196 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Instance\DisableNotificationsExpireCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Instance\ToggleInstanceFeaturesCommandForm; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +/** + * Display process and performance information of the monitoring host and program-wide commands + */ +class HealthController extends Controller +{ + /** + * Add tabs + * + * @see \Icinga\Web\Controller\ActionController::init() + */ + public function init() + { + $this + ->getTabs() + ->add( + 'info', + array( + 'title' => $this->translate( + 'Show information about the current monitoring instance\'s process' + . ' and it\'s performance as well as available features' + ), + 'label' => $this->translate('Process Information'), + 'url' =>'monitoring/health/info' + ) + ) + ->add( + 'stats', + array( + 'title' => $this->translate( + 'Show statistics about the monitored objects' + ), + 'label' => $this->translate('Stats'), + 'url' =>'monitoring/health/stats' + ) + ) + ->extend(new DashboardAction())->extend(new MenuAction()); + } + + /** + * Display process information and program-wide commands + */ + public function infoAction() + { + $this->view->title = $this->translate('Process Information'); + $this->getTabs()->activate('info'); + $this->setAutorefreshInterval(10); + $this->view->backendName = $this->backend->getName(); + $programStatus = $this->backend + ->select() + ->from( + 'programstatus', + array( + 'is_currently_running', + 'process_id', + 'endpoint_name', + 'program_start_time', + 'status_update_time', + 'program_version', + 'last_command_check', + 'last_log_rotation', + 'global_service_event_handler', + 'global_host_event_handler', + 'notifications_enabled', + 'disable_notif_expire_time', + 'active_service_checks_enabled', + 'passive_service_checks_enabled', + 'active_host_checks_enabled', + 'passive_host_checks_enabled', + 'event_handlers_enabled', + 'obsess_over_services', + 'obsess_over_hosts', + 'flap_detection_enabled', + 'process_performance_data' + ) + ) + ->getQuery(); + $this->handleFormatRequest($programStatus); + $programStatus = $programStatus->fetchRow(); + if ($programStatus === false) { + return $this->render('not-running', true, null); + } + $this->view->programStatus = $programStatus; + $toggleFeaturesForm = new ToggleInstanceFeaturesCommandForm(); + $toggleFeaturesForm + ->setBackend($this->backend) + ->setStatus($programStatus) + ->load($programStatus) + ->handleRequest(); + $this->view->toggleFeaturesForm = $toggleFeaturesForm; + + $this->view->runtimevariables = (object) $this->backend->select() + ->from('runtimevariables', array('varname', 'varvalue')) + ->getQuery()->fetchPairs(); + + $this->view->checkperformance = $this->backend->select() + ->from('runtimesummary') + ->getQuery()->fetchAll(); + } + + /** + * Display stats about current checks and monitored objects + */ + public function statsAction() + { + $this->view->title = $this->translate('Stats'); + $this->getTabs()->activate('stats'); + + $servicestats = $this->backend->select()->from('servicestatussummary', array( + 'services_critical', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning', + 'services_warning_handled', + 'services_warning_unhandled' + )); + $this->applyRestriction('monitoring/filter/objects', $servicestats); + $this->view->servicestats = $servicestats->fetchRow(); + $this->view->unhandledServiceProblems = $this->view->servicestats->services_critical_unhandled + + $this->view->servicestats->services_unknown_unhandled + + $this->view->servicestats->services_warning_unhandled; + + $hoststats = $this->backend->select()->from('hoststatussummary', array( + 'hosts_total', + 'hosts_up', + 'hosts_down', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_unreachable', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_pending', + )); + $this->applyRestriction('monitoring/filter/objects', $hoststats); + $this->view->hoststats = $hoststats->fetchRow(); + $this->view->unhandledhostProblems = $this->view->hoststats->hosts_down_unhandled + + $this->view->hoststats->hosts_unreachable_unhandled; + + $this->view->unhandledProblems = $this->view->unhandledhostProblems + + $this->view->unhandledServiceProblems; + + $this->view->runtimevariables = (object) $this->backend->select() + ->from('runtimevariables', array('varname', 'varvalue')) + ->getQuery()->fetchPairs(); + + $this->view->checkperformance = $this->backend->select() + ->from('runtimesummary') + ->getQuery()->fetchAll(); + } + + /** + * Disable notifications w/ an optional expire time + */ + public function disableNotificationsAction() + { + $this->assertPermission('monitoring/command/feature/instance'); + $this->view->title = $this->translate('Disable Notifications'); + $programStatus = $this->backend + ->select() + ->from( + 'programstatus', + array( + 'notifications_enabled', + 'disable_notif_expire_time' + ) + ) + ->getQuery() + ->fetchRow(); + $this->view->programStatus = $programStatus; + if ((bool) $programStatus->notifications_enabled === false) { + return; + } else { + $form = new DisableNotificationsExpireCommandForm(); + $form + ->setRedirectUrl('monitoring/health/info') + ->handleRequest(); + $this->view->form = $form; + } + } +} diff --git a/modules/monitoring/application/controllers/HostController.php b/modules/monitoring/application/controllers/HostController.php new file mode 100644 index 0000000..94f1a60 --- /dev/null +++ b/modules/monitoring/application/controllers/HostController.php @@ -0,0 +1,185 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostCheckCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm; +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Web\Controller\MonitoredObjectController; +use Icinga\Web\Hook; +use Icinga\Web\Navigation\Navigation; + +class HostController extends MonitoredObjectController +{ + + /** + * {@inheritdoc} + */ + protected $commandRedirectUrl = 'monitoring/host/show'; + + /** + * Fetch the requested host from the monitoring backend + */ + public function init() + { + $host = new Host($this->backend, $this->params->getRequired('host')); + $this->applyRestriction('monitoring/filter/objects', $host); + if ($host->fetch() === false) { + $this->httpNotFound($this->translate('Host not found')); + } + $this->object = $host; + $this->createTabs(); + $this->getTabs()->activate('host'); + $this->view->title = $host->host_display_name; + $this->view->defaultTitle = $this->translate('Hosts') . ' :: ' . $this->view->defaultTitle; + } + + /** + * Get host actions from hook + * + * @return Navigation + */ + protected function getHostActions() + { + $navigation = new Navigation(); + foreach (Hook::all('Monitoring\\HostActions') as $hook) { + $navigation->merge($hook->getNavigation($this->object)); + } + + return $navigation; + } + + /** + * Show a host + */ + public function showAction() + { + $this->view->actions = $this->getHostActions(); + parent::showAction(); + } + + /** + * List a host's services + */ + public function servicesAction() + { + $this->setAutorefreshInterval(10); + $this->getTabs()->activate('services'); + $query = $this->backend->select()->from('servicestatus', array( + 'host_name', + 'host_display_name', + 'host_state', + 'host_state_type', + 'host_last_state_change', + 'host_address', + 'host_address6', + 'host_handled', + 'service_description', + 'service_display_name', + 'service_state', + 'service_in_downtime', + 'service_acknowledged', + 'service_handled', + 'service_output', + 'service_perfdata', + 'service_attempt', + 'service_last_state_change', + 'service_icon_image', + 'service_icon_image_alt', + 'service_is_flapping', + 'service_state_type', + 'service_handled', + 'service_severity', + 'service_last_check', + 'service_notifications_enabled', + 'service_action_url', + 'service_notes_url', + 'service_active_checks_enabled', + 'service_passive_checks_enabled', + 'current_check_attempt' => 'service_current_check_attempt', + 'max_check_attempts' => 'service_max_check_attempts', + 'service_check_command', + 'service_next_update' + )); + $this->applyRestriction('monitoring/filter/objects', $query); + $this->view->services = $query->where('host_name', $this->object->getName()); + $this->view->object = $this->object; + } + + /** + * Acknowledge a host problem + */ + public function acknowledgeProblemAction() + { + $this->assertPermission('monitoring/command/acknowledge-problem'); + + $form = new AcknowledgeProblemCommandForm(); + $form->setTitle($this->translate('Acknowledge Host Problem')); + $this->handleCommandForm($form); + } + + /** + * Add a host comment + */ + public function addCommentAction() + { + $this->assertPermission('monitoring/command/comment/add'); + + $form = new AddCommentCommandForm(); + $form->setTitle($this->translate('Add Host Comment')); + $this->handleCommandForm($form); + } + + /** + * Reschedule a host check + */ + public function rescheduleCheckAction() + { + $this->assertPermission('monitoring/command/schedule-check'); + + $form = new ScheduleHostCheckCommandForm(); + $form->setTitle($this->translate('Reschedule Host Check')); + $this->handleCommandForm($form); + } + + /** + * Schedule a host downtime + */ + public function scheduleDowntimeAction() + { + $this->assertPermission('monitoring/command/downtime/schedule'); + + $form = new ScheduleHostDowntimeCommandForm(); + $form->setTitle($this->translate('Schedule Host Downtime')); + $this->handleCommandForm($form); + } + + /** + * Submit a passive host check result + */ + public function processCheckResultAction() + { + $this->assertPermission('monitoring/command/process-check-result'); + + $form = new ProcessCheckResultCommandForm(); + $form->setTitle($this->translate('Submit Passive Host Check Result')); + $this->handleCommandForm($form); + } + + /** + * Send a custom notification for host + */ + public function sendCustomNotificationAction() + { + $this->assertPermission('monitoring/command/send-custom-notification'); + + $form = new SendCustomNotificationCommandForm(); + $form->setTitle($this->translate('Send Custom Host Notification')); + $this->handleCommandForm($form); + } +} diff --git a/modules/monitoring/application/controllers/HostsController.php b/modules/monitoring/application/controllers/HostsController.php new file mode 100644 index 0000000..9219df8 --- /dev/null +++ b/modules/monitoring/application/controllers/HostsController.php @@ -0,0 +1,260 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Exception; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterEqual; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostCheckCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm; +use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; +use Icinga\Module\Monitoring\Object\HostList; +use Icinga\Web\Hook; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +class HostsController extends Controller +{ + /** + * @var HostList + */ + protected $hostList; + + public function init() + { + $hostList = new HostList($this->backend); + $this->applyRestriction('monitoring/filter/objects', $hostList); + $hostList->addFilter(Filter::fromQueryString((string) $this->params)); + $this->hostList = $hostList; + $this->hostList->setColumns(array( + 'host_acknowledged', + 'host_active_checks_enabled', + 'host_display_name', + 'host_event_handler_enabled', + 'host_flap_detection_enabled', + 'host_handled', + 'host_in_downtime', + 'host_is_flapping', + 'host_last_state_change', + 'host_name', + 'host_notifications_enabled', + 'host_obsessing', + 'host_passive_checks_enabled', + 'host_problem', + 'host_state', + 'instance_name' + )); + $this->view->baseFilter = $this->hostList->getFilter(); + $this->getTabs()->add( + 'show', + array( + 'label' => $this->translate('Hosts') . sprintf(' (%d)', count($this->hostList)), + 'title' => sprintf( + $this->translate('Show summarized information for %u hosts'), + count($this->hostList) + ), + 'url' => Url::fromRequest() + ) + )->extend(new DashboardAction())->extend(new MenuAction())->activate('show'); + $this->view->listAllLink = Url::fromRequest()->setPath('monitoring/list/hosts'); + $this->view->title = $this->translate('Hosts'); + } + + protected function handleCommandForm(ObjectsCommandForm $form) + { + $form + ->setBackend($this->backend) + ->setObjects($this->hostList) + ->setRedirectUrl(Url::fromPath('monitoring/hosts/show')->setParams( + $this->params->without('host_active_checks_enabled') + )) + ->handleRequest(); + + $this->view->form = $form; + $this->view->objects = $this->hostList; + $this->view->stats = $this->hostList->getStateSummary(); + $this->_helper->viewRenderer('partials/command/objects-command-form', null, true); + return $form; + } + + public function showAction() + { + $this->setAutorefreshInterval(15); + $activeChecksEnabled = $this->hostList->getFeatureStatus()['active_checks_enabled'] !== 0; + if ($this->Auth()->hasPermission('monitoring/command/schedule-check') + || ($this->Auth()->hasPermission('monitoring/command/schedule-check/active-only') + && $activeChecksEnabled + ) + ) { + $checkNowForm = new CheckNowCommandForm(); + $checkNowForm + ->setObjects($this->hostList) + ->handleRequest(); + $this->view->checkNowForm = $checkNowForm; + } + + $acknowledgedObjects = $this->hostList->getAcknowledgedObjects(); + if (! empty($acknowledgedObjects)) { + $removeAckForm = new RemoveAcknowledgementCommandForm(); + $removeAckForm + ->setObjects($acknowledgedObjects) + ->handleRequest(); + $this->view->removeAckForm = $removeAckForm; + } + + $featureStatus = $this->hostList->getFeatureStatus(); + $toggleFeaturesForm = new ToggleObjectFeaturesCommandForm(array( + 'backend' => $this->backend, + 'objects' => $this->hostList + )); + $toggleFeaturesForm + ->load((object) $featureStatus) + ->handleRequest(); + $this->view->toggleFeaturesForm = $toggleFeaturesForm; + + $hostStates = $this->hostList->getStateSummary(); + + if ($activeChecksEnabled) { + $this->view->rescheduleAllLink = Url::fromRequest() + ->setPath('monitoring/hosts/reschedule-check') + ->addParams(['host_active_checks_enabled' => true]); + } + + $this->view->downtimeAllLink = Url::fromRequest()->setPath('monitoring/hosts/schedule-downtime'); + $this->view->processCheckResultAllLink = Url::fromRequest()->setPath('monitoring/hosts/process-check-result'); + $this->view->addCommentLink = Url::fromRequest()->setPath('monitoring/hosts/add-comment'); + $this->view->stats = $hostStates; + $this->view->objects = $this->hostList; + $this->view->unhandledObjects = $this->hostList->getUnhandledObjects(); + $this->view->problemObjects = $this->hostList->getProblemObjects(); + $this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/hosts/acknowledge-problem') + ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString()); + $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/hosts/schedule-downtime') + ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString()); + $this->view->downtimeLink = Url::fromPath('monitoring/hosts/schedule-downtime') + ->setQueryString($this->hostList->getProblemObjects()->objectsFilter()->toQueryString()); + $this->view->acknowledgedObjects = $this->hostList->getAcknowledgedObjects(); + $this->view->acknowledgeLink = Url::fromPath('monitoring/hosts/acknowledge-problem') + ->setQueryString($this->hostList->getUnacknowledgedObjects()->objectsFilter()->toQueryString()); + $this->view->unacknowledgedObjects = $this->hostList->getUnacknowledgedObjects(); + $this->view->objectsInDowntime = $this->hostList->getObjectsInDowntime(); + $this->view->inDowntimeLink = Url::fromPath('monitoring/list/hosts') + ->setQueryString( + $this->hostList + ->getObjectsInDowntime() + ->objectsFilter() + ->toQueryString() + ); + $this->view->showDowntimesLink = Url::fromPath('monitoring/list/downtimes') + ->setQueryString( + $this->hostList + ->objectsFilter() + ->andFilter(FilterEqual::where('object_type', 'host')) + ->toQueryString() + ); + $this->view->commentsLink = Url::fromRequest()->setPath('monitoring/list/comments'); + $this->view->sendCustomNotificationLink = Url::fromRequest() + ->setPath('monitoring/hosts/send-custom-notification'); + + $this->view->extensionsHtml = array(); + foreach (Hook::all('Monitoring\DetailviewExtension') as $hook) { + /** @var DetailviewExtensionHook $hook */ + try { + $html = $hook->setView($this->view)->getHtmlForObjects($this->hostList); + } catch (Exception $e) { + $html = $this->view->escape($e->getMessage()); + } + + if ($html) { + $module = $this->view->escape($hook->getModule()->getName()); + $this->view->extensionsHtml[] = + '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">' + . $html + . '</div>'; + } + } + } + + /** + * Add a host comments + */ + public function addCommentAction() + { + $this->assertPermission('monitoring/command/comment/add'); + + $form = new AddCommentCommandForm(); + $form->setTitle($this->translate('Add Host Comments')); + $this->handleCommandForm($form); + } + + /** + * Acknowledge host problems + */ + public function acknowledgeProblemAction() + { + $this->assertPermission('monitoring/command/acknowledge-problem'); + + $form = new AcknowledgeProblemCommandForm(); + $form->setTitle($this->translate('Acknowledge Host Problems')); + $this->handleCommandForm($form); + } + + /** + * Reschedule host checks + */ + public function rescheduleCheckAction() + { + $this->assertPermission('monitoring/command/schedule-check'); + + $form = new ScheduleHostCheckCommandForm(); + $form->setTitle($this->translate('Reschedule Host Checks')); + $this->handleCommandForm($form); + } + + /** + * Schedule host downtimes + */ + public function scheduleDowntimeAction() + { + $this->assertPermission('monitoring/command/downtime/schedule'); + + $form = new ScheduleHostDowntimeCommandForm(); + $form->setTitle($this->translate('Schedule Host Downtimes')); + $this->handleCommandForm($form); + } + + /** + * Submit passive host check results + */ + public function processCheckResultAction() + { + $this->assertPermission('monitoring/command/process-check-result'); + + $form = new ProcessCheckResultCommandForm(); + $form->setTitle($this->translate('Submit Passive Host Check Results')); + $this->handleCommandForm($form); + } + + /** + * Send a custom notification for hosts + */ + public function sendCustomNotificationAction() + { + $this->assertPermission('monitoring/command/send-custom-notification'); + + $form = new SendCustomNotificationCommandForm(); + $form->setTitle($this->translate('Send Custom Host Notification')); + $this->handleCommandForm($form); + } +} diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php new file mode 100644 index 0000000..0ccff99 --- /dev/null +++ b/modules/monitoring/application/controllers/ListController.php @@ -0,0 +1,808 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Security\SecurityException; +use Icinga\Util\GlobFilter; +use Icinga\Web\Form; +use Zend_Form; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\DataView\DataView; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\StatehistoryForm; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; +use Icinga\Web\Widget\Tabextension\OutputFormat; +use Icinga\Web\Widget\Tabs; + +class ListController extends Controller +{ + /** + * @see ActionController::init + */ + public function init() + { + parent::init(); + $this->createTabs(); + } + + /** + * Overwrite the backend to use (used for testing) + * + * @param MonitoringBackend $backend The Backend that should be used for querying + */ + public function setBackend($backend) + { + $this->backend = $backend; + } + + /** + * List hosts + */ + public function hostsAction() + { + $this->addTitleTab( + 'hosts', + $this->translate('Hosts'), + $this->translate('List hosts') + ); + + $this->setAutorefreshInterval(10); + + // Handle soft and hard states + if (strtolower($this->params->shift('stateType', 'soft')) === 'hard') { + $stateColumn = 'host_hard_state'; + $stateChangeColumn = 'host_last_hard_state_change'; + } else { + $stateColumn = 'host_state'; + $stateChangeColumn = 'host_last_state_change'; + } + + $hosts = $this->backend->select()->from('hoststatus', array_merge(array( + 'host_icon_image', + 'host_icon_image_alt', + 'host_name', + 'host_display_name', + 'host_state' => $stateColumn, + 'host_acknowledged', + 'host_output', + 'host_attempt', + 'host_in_downtime', + 'host_is_flapping', + 'host_state_type', + 'host_handled', + 'host_last_state_change' => $stateChangeColumn, + 'host_notifications_enabled', + 'host_active_checks_enabled', + 'host_passive_checks_enabled', + 'host_check_command', + 'host_next_update' + ), $this->addColumns())); + + $this->setupPaginationControl($hosts); + $this->setupSortControl(array( + 'host_severity' => $this->translate('Severity'), + 'host_state' => $this->translate('Current State'), + 'host_display_name' => $this->translate('Hostname'), + 'host_address' => $this->translate('Address'), + 'host_last_check' => $this->translate('Last Check'), + 'host_last_state_change' => $this->translate('Last State Change') + ), $hosts); + $this->filterQuery($hosts); + $this->setupLimitControl(); + + $stats = $this->backend->select()->from('hoststatussummary', array( + 'hosts_total', + 'hosts_up', + 'hosts_down', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_unreachable', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_pending', + )); + $this->applyRestriction('monitoring/filter/objects', $stats); + + $this->view->hosts = $hosts; + $this->view->stats = $stats; + } + + /** + * List services + */ + public function servicesAction() + { + $this->addTitleTab( + 'services', + $this->translate('Services'), + $this->translate('List services') + ); + + // Handle soft and hard states + if (strtolower($this->params->shift('stateType', 'soft')) === 'hard') { + $stateColumn = 'service_hard_state'; + $stateChangeColumn = 'service_last_hard_state_change'; + } else { + $stateColumn = 'service_state'; + $stateChangeColumn = 'service_last_state_change'; + } + + $this->setAutorefreshInterval(10); + + $services = $this->backend->select()->from('servicestatus', array_merge(array( + 'host_name', + 'host_display_name', + 'host_state', + 'service_description', + 'service_display_name', + 'service_state' => $stateColumn, + 'service_in_downtime', + 'service_acknowledged', + 'service_handled', + 'service_output', + 'service_perfdata', + 'service_attempt', + 'service_last_state_change' => $stateChangeColumn, + 'service_icon_image', + 'service_icon_image_alt', + 'service_is_flapping', + 'service_state_type', + 'service_handled', + 'service_severity', + 'service_notifications_enabled', + 'service_active_checks_enabled', + 'service_passive_checks_enabled', + 'service_check_command', + 'service_next_update' + ), $this->addColumns())); + + $this->setupPaginationControl($services); + $this->setupSortControl(array( + 'service_severity' => $this->translate('Service Severity'), + 'service_state' => $this->translate('Current Service State'), + 'service_display_name' => $this->translate('Service Name'), + 'service_last_check' => $this->translate('Last Service Check'), + 'service_last_state_change' => $this->translate('Last State Change'), + 'host_severity' => $this->translate('Host Severity'), + 'host_state' => $this->translate('Current Host State'), + 'host_display_name' => $this->translate('Hostname'), + 'host_address' => $this->translate('Host Address'), + 'host_last_check' => $this->translate('Last Host Check') + ), $services); + $this->filterQuery($services); + $this->setupLimitControl(); + + $stats = $this->backend->select()->from('servicestatussummary', array( + 'services_critical', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning', + 'services_warning_handled', + 'services_warning_unhandled' + )); + $this->applyRestriction('monitoring/filter/objects', $stats); + + $this->view->services = $services; + $this->view->stats = $stats; + if (strpos($this->params->get('host_name', '*'), '*') === false) { + $this->view->showHost = false; + } else { + $this->view->showHost = true; + } + } + + /** + * List downtimes + */ + public function downtimesAction() + { + $this->addTitleTab( + 'downtimes', + $this->translate('Downtimes'), + $this->translate('List downtimes') + ); + + $this->setAutorefreshInterval(12); + + $downtimes = $this->backend->select()->from('downtime', array( + 'id' => 'downtime_internal_id', + 'objecttype' => 'object_type', + 'comment' => 'downtime_comment', + 'author_name' => 'downtime_author_name', + 'start' => 'downtime_start', + 'scheduled_start' => 'downtime_scheduled_start', + 'scheduled_end' => 'downtime_scheduled_end', + 'end' => 'downtime_end', + 'duration' => 'downtime_duration', + 'is_flexible' => 'downtime_is_flexible', + 'is_fixed' => 'downtime_is_fixed', + 'is_in_effect' => 'downtime_is_in_effect', + 'entry_time' => 'downtime_entry_time', + 'name' => 'downtime_name', + 'host_state', + 'service_state', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + )); + + $this->setupPaginationControl($downtimes); + $this->setupSortControl(array( + 'downtime_is_in_effect' => $this->translate('Is In Effect'), + 'host_display_name' => $this->translate('Host'), + 'service_display_name' => $this->translate('Service'), + 'downtime_entry_time' => $this->translate('Entry Time'), + 'downtime_author' => $this->translate('Author'), + 'downtime_start' => $this->translate('Start Time'), + 'downtime_end' => $this->translate('End Time'), + 'downtime_scheduled_start' => $this->translate('Scheduled Start'), + 'downtime_scheduled_end' => $this->translate('Scheduled End'), + 'downtime_duration' => $this->translate('Duration') + ), $downtimes); + $this->filterQuery($downtimes); + $this->setupLimitControl(); + + $this->view->downtimes = $downtimes; + + if ($this->Auth()->hasPermission('monitoring/command/downtime/delete')) { + $this->view->delDowntimeForm = new DeleteDowntimeCommandForm(); + $this->view->delDowntimeForm->handleRequest(); + } + } + + /** + * List notifications + */ + public function notificationsAction() + { + $this->addTitleTab( + 'notifications', + $this->translate('Notifications'), + $this->translate('List notifications') + ); + + $this->setAutorefreshInterval(15); + + $notifications = $this->backend->select()->from('notification', array( + 'id', + 'host_display_name', + 'host_name', + 'notification_contact_name', + 'notification_output', + 'notification_state', + 'notification_timestamp', + 'service_description', + 'service_display_name' + )); + + $this->setupPaginationControl($notifications); + $this->setupSortControl(array( + 'notification_timestamp' => $this->translate('Notification Start') + ), $notifications); + $this->filterQuery($notifications); + $this->setupLimitControl(); + + $this->view->notifications = $notifications; + } + + /** + * List contacts + */ + public function contactsAction() + { + if (! $this->hasPermission('*') && $this->hasPermission('no-monitoring/contacts')) { + throw new SecurityException('No permission for %s', 'monitoring/contacts'); + } + + $this->addTitleTab( + 'contacts', + $this->translate('Contacts'), + $this->translate('List contacts') + ); + + $contacts = $this->backend->select()->from('contact', array( + 'contact_name', + 'contact_alias', + 'contact_email', + 'contact_pager', + 'contact_notify_service_timeperiod', + 'contact_notify_host_timeperiod' + )); + + $this->setupPaginationControl($contacts); + $this->setupSortControl(array( + 'contact_name' => $this->translate('Name'), + 'contact_alias' => $this->translate('Alias'), + 'contact_email' => $this->translate('Email'), + 'contact_pager' => $this->translate('Pager Address / Number') + ), $contacts); + $this->filterQuery($contacts); + $this->setupLimitControl(); + + $this->view->contacts = $contacts; + } + + public function eventgridAction() + { + $this->addTitleTab('eventgrid', $this->translate('Event Grid'), $this->translate('Show the Event Grid')); + + $form = new StatehistoryForm(); + $form->setEnctype(Zend_Form::ENCTYPE_URLENCODED); + $form->setMethod('get'); + $form->setTokenDisabled(); + $form->setUidDisabled(); + $form->render(); + $this->view->form = $form; + + $this->params + ->remove('showCompact') + ->remove('format'); + $orientation = $this->params->shift('vertical', 0) ? 'vertical' : 'horizontal'; +/* + $orientationBox = new SelectBox( + 'orientation', + array( + '0' => mt('monitoring', 'Vertical'), + '1' => mt('monitoring', 'Horizontal') + ), + mt('monitoring', 'Orientation'), + 'horizontal' + ); + $orientationBox->applyRequest($this->getRequest()); +*/ + $objectType = $form->getValue('objecttype'); + $from = $form->getValue('from'); + $query = $this->backend->select()->from( + 'eventgrid' . $objectType, + array('day', $form->getValue('state')) + ); + $this->params->remove(array('objecttype', 'from', 'to', 'state', 'btn_submit')); + $this->view->filter = Filter::fromQuerystring((string) $this->params); + $query->applyFilter($this->view->filter); + $query->applyFilter(Filter::fromQuerystring('timestamp>=' . $from)); + $this->applyRestriction('monitoring/filter/objects', $query); + $this->view->summary = $query; + $this->view->column = $form->getValue('state'); +// $this->view->orientationBox = $orientationBox; + $this->view->orientation = $orientation; + } + + /** + * List contact groups + */ + public function contactgroupsAction() + { + if (! $this->hasPermission('*') && $this->hasPermission('no-monitoring/contacts')) { + throw new SecurityException('No permission for %s', 'monitoring/contacts'); + } + + $this->addTitleTab( + 'contactgroups', + $this->translate('Contact Groups'), + $this->translate('List contact groups') + ); + + $contactGroups = $this->backend->select()->from('contactgroup', array( + 'contactgroup_name', + 'contactgroup_alias', + 'contact_count' + )); + + $this->setupPaginationControl($contactGroups); + $this->setupSortControl(array( + 'contactgroup_name' => $this->translate('Contactgroup Name'), + 'contactgroup_alias' => $this->translate('Contactgroup Alias') + ), $contactGroups); + $this->filterQuery($contactGroups); + $this->setupLimitControl(); + + $this->view->contactGroups = $contactGroups; + } + + /** + * List all comments + */ + public function commentsAction() + { + $this->addTitleTab( + 'comments', + $this->translate('Comments'), + $this->translate('List comments') + ); + + $this->setAutorefreshInterval(12); + + $comments = $this->backend->select()->from('comment', array( + 'id' => 'comment_internal_id', + 'objecttype' => 'object_type', + 'comment' => 'comment_data', + 'author' => 'comment_author_name', + 'timestamp' => 'comment_timestamp', + 'type' => 'comment_type', + 'persistent' => 'comment_is_persistent', + 'expiration' => 'comment_expiration', + 'name' => 'comment_name', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + )); + + $this->setupPaginationControl($comments); + $this->setupSortControl( + array( + 'comment_timestamp' => $this->translate('Comment Timestamp'), + 'host_display_name' => $this->translate('Host'), + 'service_display_name' => $this->translate('Service'), + 'comment_type' => $this->translate('Comment Type'), + 'comment_expiration' => $this->translate('Expiration') + ), + $comments + ); + $this->filterQuery($comments); + $this->setupLimitControl(); + + $this->view->comments = $comments; + + if ($this->Auth()->hasPermission('monitoring/command/comment/delete')) { + $this->view->delCommentForm = new DeleteCommentCommandForm(); + $this->view->delCommentForm->handleRequest(); + } + } + + /** + * List service groups + */ + public function servicegroupsAction() + { + $this->addTitleTab( + 'servicegroups', + $this->translate('Service Groups'), + $this->translate('List service groups') + ); + + $this->setAutorefreshInterval(12); + + $serviceGroups = $this->backend->select()->from('servicegroupsummary', array( + 'servicegroup_alias', + 'servicegroup_name', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning_handled', + 'services_warning_unhandled' + )); + + $this->setupPaginationControl($serviceGroups); + $this->setupSortControl(array( + 'servicegroup_alias' => $this->translate('Service Group Name'), + 'services_severity' => $this->translate('Severity'), + 'services_total' => $this->translate('Total Services') + ), $serviceGroups); + $this->filterQuery($serviceGroups); + $this->setupLimitControl(); + + $this->view->serviceGroups = $serviceGroups; + } + + /** + * List service groups + */ + public function servicegroupGridAction() + { + $this->addTitleTab( + 'servicegroup-grid', + $this->translate('Service Group Grid'), + $this->translate('Show the Service Group Grid') + ); + + $this->setAutorefreshInterval(15); + + $serviceGroups = $this->backend->select()->from('servicegroupsummary', array( + 'servicegroup_alias', + 'servicegroup_name', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning_handled', + 'services_warning_unhandled' + )); + $this->filterQuery($serviceGroups); + + $this->setupSortControl(array( + 'servicegroup_alias' => $this->translate('Service Group Name'), + 'services_severity' => $this->translate('Severity'), + 'services_total' => $this->translate('Total Services') + ), $serviceGroups, ['services_severity' => 'desc']); + + $this->view->serviceGroups = $serviceGroups; + } + + /** + * List host groups + */ + public function hostgroupsAction() + { + $this->addTitleTab( + 'hostgroups', + $this->translate('Host Groups'), + $this->translate('List host groups') + ); + + $this->setAutorefreshInterval(12); + + $hostGroups = $this->backend->select()->from('hostgroupsummary', array( + 'hostgroup_alias', + 'hostgroup_name', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_pending', + 'hosts_total', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_up', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning_handled', + 'services_warning_unhandled' + )); + + $this->setupPaginationControl($hostGroups); + $this->setupSortControl(array( + 'hostgroup_alias' => $this->translate('Host Group Name'), + 'hosts_severity' => $this->translate('Severity'), + 'hosts_total' => $this->translate('Total Hosts'), + 'services_total' => $this->translate('Total Services') + ), $hostGroups); + $this->filterQuery($hostGroups); + $this->setupLimitControl(); + + $this->view->hostGroups = $hostGroups; + } + + /** + * List host groups + */ + public function hostgroupGridAction() + { + $this->addTitleTab( + 'hostgroup-grid', + $this->translate('Host Group Grid'), + $this->translate('Show the Host Group Grid') + ); + + $this->setAutorefreshInterval(15); + + $hostGroups = $this->backend->select()->from('hostgroupsummary', [ + 'hostgroup_alias', + 'hostgroup_name', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_pending', + 'hosts_total', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_up' + ]); + $this->filterQuery($hostGroups); + + $this->setupSortControl([ + 'hosts_severity' => $this->translate('Severity'), + 'hostgroup_alias' => $this->translate('Host Group Name'), + 'hosts_total' => $this->translate('Total Hosts'), + 'services_total' => $this->translate('Total Services') + ], $hostGroups, ['hosts_severity' => 'desc']); + + $this->view->hostGroups = $hostGroups; + } + + public function eventhistoryAction() + { + $this->addTitleTab( + 'eventhistory', + $this->translate('Event Overview'), + $this->translate('List event records') + ); + + $query = $this->backend->select()->from('eventhistory', array( + 'id', + 'host_name', + 'host_display_name', + 'service_description', + 'service_display_name', + 'object_type', + 'timestamp', + 'state', + 'output', + 'type' + )); + + $this->view->history = $query; + + $this->setupSortControl(array( + 'timestamp' => $this->translate('Occurence') + ), $query); + $this->filterQuery($query); + $this->setupLimitControl(); + } + + public function servicegridAction() + { + if ($this->params->has('noscript_apply')) { + $this->redirectNow($this->getRequest()->getUrl()->without('noscript_apply')); + } + + $this->addTitleTab('servicegrid', $this->translate('Service Grid'), $this->translate('Show the Service Grid')); + $this->setAutorefreshInterval(15); + $query = $this->backend->select()->from('servicestatus', array( + 'host_display_name', + 'host_name', + 'service_description', + 'service_display_name', + 'service_handled', + 'service_output', + 'service_state' + )); + $this->filterQuery($query); + $filter = (bool) $this->params->shift('problems', false) ? Filter::where('service_problem', 1) : null; + + $this->view->problemToggle = $problemToggle = new Form(['method' => 'GET']); + $problemToggle->setUidDisabled(); + $problemToggle->setTokenDisabled(); + $problemToggle->setAttrib('class', 'filter-toggle inline icinga-controls'); + $problemToggle->addElement('checkbox', 'problems', [ + 'disableHidden' => true, + 'autosubmit' => true, + 'value' => $filter !== null, + 'label' => $this->translate('Problems Only'), + 'decorators' => ['ViewHelper', ['Label', ['placement' => 'APPEND']]] + ]); + + if ($this->params->get('flipped', false)) { + $pivot = $query + ->pivot( + 'host_name', + 'service_description', + $filter, + $filter ? clone $filter : null + ) + ->setYAxisHeader('service_display_name') + ->setXAxisHeader('host_display_name'); + } else { + $pivot = $query + ->pivot( + 'service_description', + 'host_name', + $filter, + $filter ? clone $filter : null + ) + ->setXAxisHeader('service_display_name') + ->setYAxisHeader('host_display_name'); + } + $this->setupSortControl(array( + 'host_display_name' => $this->translate('Hostname'), + 'service_display_name' => $this->translate('Service Name') + ), $pivot); + $this->view->horizontalPaginator = $pivot->paginateXAxis(); + $this->view->verticalPaginator = $pivot->paginateYAxis(); + list($pivotData, $pivotHeader) = $pivot->toArray(); + $this->view->pivotData = $pivotData; + $this->view->pivotHeader = $pivotHeader; + if ($this->params->get('flipped', false)) { + $this->render('servicegrid-flipped'); + } + } + + /** + * Apply filters on a DataView + * + * @param DataView $dataView The DataView to apply filters on + * + * @return DataView $dataView + */ + protected function filterQuery(DataView $dataView) + { + $this->setupFilterControl($dataView, null, null, array( + 'format', // handleFormatRequest() + 'stateType', // hostsAction() and servicesAction() + 'addColumns', // addColumns() + 'problems', // servicegridAction() + 'flipped' // servicegridAction() + )); + + if ($this->params->get('format') !== 'sql' || $this->hasPermission('config/authentication/roles/show')) { + $this->applyRestriction('monitoring/filter/objects', $dataView); + } + + $this->handleFormatRequest($dataView); + + return $dataView; + } + + /** + * Get columns to be added from URL parameter 'addColumns' + * and assign to $this->view->addColumns (as array) + * + * @return array + */ + protected function addColumns() + { + $columns = preg_split( + '~,~', + $this->params->shift('addColumns', ''), + -1, + PREG_SPLIT_NO_EMPTY + ); + + $customVars = []; + $additionalCols = []; + foreach ($columns as $column) { + if (preg_match('~^_(host|service)_([a-zA-Z0-9_]+)$~', $column, $m)) { + $customVars[$m[1]]['vars'][$m[2]] = null; + } else { + $additionalCols[] = $column; + } + } + + if (! empty($customVars)) { + $blacklistedProperties = new GlobFilter( + $this->getRestrictions('monitoring/blacklist/properties') + ); + $customVars = $blacklistedProperties->removeMatching($customVars); + foreach ($customVars as $type => $vars) { + foreach ($vars['vars'] as $var => $_) { + $additionalCols[] = '_' . $type . '_' . $var; + } + } + } + + $this->view->addColumns = $additionalCols; + return $additionalCols; + } + + protected function addTitleTab($action, $title, $tip) + { + $this->getTabs()->add($action, array( + 'title' => $tip, + 'label' => $title, + 'url' => Url::fromRequest() + ))->activate($action); + $this->view->title = $title; + } + + /** + * Return all tabs for this controller + * + * @return Tabs + */ + private function createTabs() + { + $this->getTabs()->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction()); + } +} diff --git a/modules/monitoring/application/controllers/ServiceController.php b/modules/monitoring/application/controllers/ServiceController.php new file mode 100644 index 0000000..d3eeb1c --- /dev/null +++ b/modules/monitoring/application/controllers/ServiceController.php @@ -0,0 +1,147 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceCheckCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Module\Monitoring\Web\Controller\MonitoredObjectController; +use Icinga\Web\Hook; +use Icinga\Web\Navigation\Navigation; + +class ServiceController extends MonitoredObjectController +{ + /** + * {@inheritdoc} + */ + protected $commandRedirectUrl = 'monitoring/service/show'; + + /** + * Fetch the requested service from the monitoring backend + */ + public function init() + { + $service = new Service( + $this->backend, + $this->params->getRequired('host'), + $this->params->getRequired('service') + ); + + $this->applyRestriction('monitoring/filter/objects', $service); + + if ($service->fetch() === false) { + $this->httpNotFound($this->translate('Service not found')); + } + $this->object = $service; + $this->createTabs(); + $this->getTabs()->activate('service'); + $this->view->title = $service->service_display_name; + $this->view->defaultTitle = join(' :: ', [ + $service->host_display_name, + $this->translate('Services'), + $this->view->defaultTitle + ]); + } + + /** + * Get service actions from hook + * + * @return Navigation + */ + protected function getServiceActions() + { + $navigation = new Navigation(); + foreach (Hook::all('Monitoring\\ServiceActions') as $hook) { + $navigation->merge($hook->getNavigation($this->object)); + } + + return $navigation; + } + + /** + * Show a service + */ + public function showAction() + { + $this->view->actions = $this->getServiceActions(); + parent::showAction(); + } + + + /** + * Acknowledge a service problem + */ + public function acknowledgeProblemAction() + { + $this->assertPermission('monitoring/command/acknowledge-problem'); + + $form = new AcknowledgeProblemCommandForm(); + $form->setTitle($this->translate('Acknowledge Service Problem')); + $this->handleCommandForm($form); + } + + /** + * Add a service comment + */ + public function addCommentAction() + { + $this->assertPermission('monitoring/command/comment/add'); + + $form = new AddCommentCommandForm(); + $form->setTitle($this->translate('Add Service Comment')); + $this->handleCommandForm($form); + } + + /** + * Reschedule a service check + */ + public function rescheduleCheckAction() + { + $this->assertPermission('monitoring/command/schedule-check'); + + $form = new ScheduleServiceCheckCommandForm(); + $form->setTitle($this->translate('Reschedule Service Check')); + $this->handleCommandForm($form); + } + + /** + * Schedule a service downtime + */ + public function scheduleDowntimeAction() + { + $this->assertPermission('monitoring/command/downtime/schedule'); + + $form = new ScheduleServiceDowntimeCommandForm(); + $form->setTitle($this->translate('Schedule Service Downtime')); + $this->handleCommandForm($form); + } + + /** + * Submit a passive service check result + */ + public function processCheckResultAction() + { + $this->assertPermission('monitoring/command/process-check-result'); + + $form = new ProcessCheckResultCommandForm(); + $form->setTitle($this->translate('Submit Passive Service Check Result')); + $this->handleCommandForm($form); + } + + /** + * Send a custom notification for a service + */ + public function sendCustomNotificationAction() + { + $this->assertPermission('monitoring/command/send-custom-notification'); + + $form = new SendCustomNotificationCommandForm(); + $form->setTitle($this->translate('Send Custom Service Notification')); + $this->handleCommandForm($form); + } +} diff --git a/modules/monitoring/application/controllers/ServicesController.php b/modules/monitoring/application/controllers/ServicesController.php new file mode 100644 index 0000000..6c65592 --- /dev/null +++ b/modules/monitoring/application/controllers/ServicesController.php @@ -0,0 +1,262 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Exception; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceCheckCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm; +use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; +use Icinga\Module\Monitoring\Object\ServiceList; +use Icinga\Web\Hook; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +class ServicesController extends Controller +{ + /** + * @var ServiceList + */ + protected $serviceList; + + public function init() + { + $serviceList = new ServiceList($this->backend); + $this->applyRestriction('monitoring/filter/objects', $serviceList); + $serviceList->addFilter(Filter::fromQueryString( + (string) $this->params->without(array('service_problem', 'service_handled', 'showCompact')) + )); + $this->serviceList = $serviceList; + $this->serviceList->setColumns(array( + 'host_display_name', + 'host_handled', + 'host_name', + 'host_problem', + 'host_state', + 'instance_name', + 'service_acknowledged', + 'service_active_checks_enabled', + 'service_description', + 'service_display_name', + 'service_event_handler_enabled', + 'service_flap_detection_enabled', + 'service_handled', + 'service_in_downtime', + 'service_is_flapping', + 'service_last_state_change', + 'service_notifications_enabled', + 'service_obsessing', + 'service_passive_checks_enabled', + 'service_problem', + 'service_state' + )); + $this->view->baseFilter = $this->serviceList->getFilter(); + $this->view->listAllLink = Url::fromRequest()->setPath('monitoring/list/services'); + $this->getTabs()->add( + 'show', + array( + 'label' => $this->translate('Services') . sprintf(' (%d)', count($this->serviceList)), + 'title' => sprintf( + $this->translate('Show summarized information for %u services'), + count($this->serviceList) + ), + 'url' => Url::fromRequest() + ) + )->extend(new DashboardAction())->extend(new MenuAction())->activate('show'); + $this->view->title = $this->translate('Services'); + } + + protected function handleCommandForm(ObjectsCommandForm $form) + { + $form + ->setBackend($this->backend) + ->setObjects($this->serviceList) + ->setRedirectUrl(Url::fromPath('monitoring/services/show')->setParams( + $this->params->without('service_active_checks_enabled') + )) + ->handleRequest(); + + $this->view->form = $form; + $this->view->objects = $this->serviceList; + $this->view->stats = $this->serviceList->getServiceStateSummary(); + $this->view->serviceStates = true; + $this->_helper->viewRenderer('partials/command/objects-command-form', null, true); + return $form; + } + + public function showAction() + { + $this->setAutorefreshInterval(15); + $activeChecksEnabled = $this->serviceList->getFeatureStatus()['active_checks_enabled'] !== 0; + if ($this->Auth()->hasPermission('monitoring/command/schedule-check') + || ($this->Auth()->hasPermission('monitoring/command/schedule-check/active-only') + && $activeChecksEnabled + ) + ) { + $checkNowForm = new CheckNowCommandForm(); + $checkNowForm + ->setObjects($this->serviceList) + ->handleRequest(); + $this->view->checkNowForm = $checkNowForm; + } + + $acknowledgedObjects = $this->serviceList->getAcknowledgedObjects(); + if (! empty($acknowledgedObjects)) { + $removeAckForm = new RemoveAcknowledgementCommandForm(); + $removeAckForm + ->setObjects($acknowledgedObjects) + ->handleRequest(); + $this->view->removeAckForm = $removeAckForm; + } + + $featureStatus = $this->serviceList->getFeatureStatus(); + $toggleFeaturesForm = new ToggleObjectFeaturesCommandForm(array( + 'backend' => $this->backend, + 'objects' => $this->serviceList + )); + $toggleFeaturesForm + ->load((object) $featureStatus) + ->handleRequest(); + $this->view->toggleFeaturesForm = $toggleFeaturesForm; + + if ($activeChecksEnabled) { + $this->view->rescheduleAllLink = Url::fromRequest() + ->setPath('monitoring/services/reschedule-check') + ->addParams(['service_active_checks_enabled' => true]); + } + + $this->view->downtimeAllLink = Url::fromRequest()->setPath('monitoring/services/schedule-downtime'); + $this->view->processCheckResultAllLink = Url::fromRequest()->setPath( + 'monitoring/services/process-check-result' + ); + $this->view->addCommentLink = Url::fromRequest()->setPath('monitoring/services/add-comment'); + $this->view->deleteCommentLink = Url::fromRequest()->setPath('monitoring/services/delete-comment'); + $this->view->stats = $this->serviceList->getServiceStateSummary(); + $this->view->objects = $this->serviceList; + $this->view->unhandledObjects = $this->serviceList->getUnhandledObjects(); + $this->view->problemObjects = $this->serviceList->getProblemObjects(); + $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/services/schedule-downtime') + ->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString()); + $this->view->downtimeLink = Url::fromPath('monitoring/services/schedule-downtime') + ->setQueryString($this->serviceList->getProblemObjects()->objectsFilter()->toQueryString()); + $this->view->acknowledgedObjects = $acknowledgedObjects; + $this->view->acknowledgeLink = Url::fromPath('monitoring/services/acknowledge-problem') + ->setQueryString($this->serviceList->getUnacknowledgedObjects()->objectsFilter()->toQueryString()); + $this->view->unacknowledgedObjects = $this->serviceList->getUnacknowledgedObjects(); + $this->view->objectsInDowntime = $this->serviceList->getObjectsInDowntime(); + $this->view->inDowntimeLink = Url::fromPath('monitoring/list/services') + ->setQueryString($this->serviceList->getObjectsInDowntime() + ->objectsFilter(array('host' => 'host_name', 'service' => 'service_description'))->toQueryString()); + $this->view->showDowntimesLink = Url::fromPath('monitoring/downtimes/show') + ->setQueryString( + $this->serviceList->getObjectsInDowntime() + ->objectsFilter()->andFilter(Filter::where('object_type', 'service'))->toQueryString() + ); + $this->view->commentsLink = Url::fromRequest() + ->setPath('monitoring/list/comments'); + $this->view->sendCustomNotificationLink = Url::fromRequest()->setPath( + 'monitoring/services/send-custom-notification' + ); + + $this->view->extensionsHtml = array(); + foreach (Hook::all('Monitoring\DetailviewExtension') as $hook) { + /** @var DetailviewExtensionHook $hook */ + try { + $html = $hook->setView($this->view)->getHtmlForObjects($this->serviceList); + } catch (Exception $e) { + $html = $this->view->escape($e->getMessage()); + } + + if ($html) { + $module = $this->view->escape($hook->getModule()->getName()); + $this->view->extensionsHtml[] = + '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">' + . $html + . '</div>'; + } + } + } + + /** + * Add a service comment + */ + public function addCommentAction() + { + $this->assertPermission('monitoring/command/comment/add'); + + $form = new AddCommentCommandForm(); + $form->setTitle($this->translate('Add Service Comments')); + $this->handleCommandForm($form); + } + + /** + * Acknowledge service problems + */ + public function acknowledgeProblemAction() + { + $this->assertPermission('monitoring/command/acknowledge-problem'); + + $form = new AcknowledgeProblemCommandForm(); + $form->setTitle($this->translate('Acknowledge Service Problems')); + $this->handleCommandForm($form); + } + + /** + * Reschedule service checks + */ + public function rescheduleCheckAction() + { + $this->assertPermission('monitoring/command/schedule-check'); + + $form = new ScheduleServiceCheckCommandForm(); + $form->setTitle($this->translate('Reschedule Service Checks')); + $this->handleCommandForm($form); + } + + /** + * Schedule service downtimes + */ + public function scheduleDowntimeAction() + { + $this->assertPermission('monitoring/command/downtime/schedule'); + + $form = new ScheduleServiceDowntimeCommandForm(); + $form->setTitle($this->translate('Schedule Service Downtimes')); + $this->handleCommandForm($form); + } + + /** + * Submit passive service check results + */ + public function processCheckResultAction() + { + $this->assertPermission('monitoring/command/process-check-result'); + + $form = new ProcessCheckResultCommandForm(); + $form->setTitle($this->translate('Submit Passive Service Check Results')); + $this->handleCommandForm($form); + } + + /** + * Send a custom notification for services + */ + public function sendCustomNotificationAction() + { + $this->assertPermission('monitoring/command/send-custom-notification'); + + $form = new SendCustomNotificationCommandForm(); + $form->setTitle($this->translate('Send Custom Service Notification')); + $this->handleCommandForm($form); + } +} diff --git a/modules/monitoring/application/controllers/ShowController.php b/modules/monitoring/application/controllers/ShowController.php new file mode 100644 index 0000000..f1da561 --- /dev/null +++ b/modules/monitoring/application/controllers/ShowController.php @@ -0,0 +1,101 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Data\Filter\FilterEqual; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Controller; +use Icinga\Security\SecurityException; +use Icinga\Web\Url; + +/** + * Class Monitoring_ShowController + * + * Actions for show context + */ +class ShowController extends Controller +{ + /** + * @var MonitoringBackend + */ + protected $backend; + + public function init() + { + $this->view->defaultTitle = $this->translate('Contacts') . ' :: ' . $this->view->defaultTitle; + + parent::init(); + } + + public function contactAction() + { + if (! $this->hasPermission('*') && $this->hasPermission('no-monitoring/contacts')) { + throw new SecurityException('No permission for %s', 'monitoring/contacts'); + } + + $contactName = $this->params->getRequired('contact_name'); + + $this->getTabs()->add('contact-detail', [ + 'title' => $this->translate('Contact details'), + 'label' => $this->translate('Contact'), + 'url' => Url::fromRequest(), + 'active' => true + ]); + + $query = $this->backend->select()->from('contact', array( + 'contact_name', + 'contact_id', + 'contact_alias', + 'contact_email', + 'contact_pager', + 'contact_notify_service_timeperiod', + 'contact_notify_service_recovery', + 'contact_notify_service_warning', + 'contact_notify_service_critical', + 'contact_notify_service_unknown', + 'contact_notify_service_flapping', + 'contact_notify_service_downtime', + 'contact_notify_host_timeperiod', + 'contact_notify_host_recovery', + 'contact_notify_host_down', + 'contact_notify_host_unreachable', + 'contact_notify_host_flapping', + 'contact_notify_host_downtime', + )); + $this->applyRestriction('monitoring/filter/objects', $query); + $query->whereEx(new FilterEqual('contact_name', '=', $contactName)); + $contact = $query->getQuery()->fetchRow(); + + if ($contact) { + $commands = $this->backend->select()->from('command', array( + 'command_line', + 'command_name' + ))->where('contact_id', $contact->contact_id); + + $this->view->commands = $commands; + + $notifications = $this->backend->select()->from('notification', array( + 'id', + 'host_name', + 'service_description', + 'notification_output', + 'notification_contact_name', + 'notification_timestamp', + 'notification_state', + 'host_display_name', + 'service_display_name' + )); + + $notifications->where('notification_contact_name', $contactName); + $this->applyRestriction('monitoring/filter/objects', $notifications); + $this->view->notifications = $notifications; + $this->setupLimitControl(); + $this->setupPaginationControl($this->view->notifications); + $this->view->title = $contact->contact_name; + } + + $this->view->contact = $contact; + $this->view->contactName = $contactName; + } +} diff --git a/modules/monitoring/application/controllers/TacticalController.php b/modules/monitoring/application/controllers/TacticalController.php new file mode 100644 index 0000000..b147d45 --- /dev/null +++ b/modules/monitoring/application/controllers/TacticalController.php @@ -0,0 +1,128 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use Icinga\Chart\Donut; +use Icinga\Module\Monitoring\Controller; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +class TacticalController extends Controller +{ + public function indexAction() + { + $this->setAutorefreshInterval(15); + + $this->view->title = $this->translate('Tactical Overview'); + $this->getTabs()->add( + 'tactical_overview', + array( + 'title' => $this->translate( + 'Show an overview of all hosts and services, their current' + . ' states and monitoring feature utilisation' + ), + 'label' => $this->translate('Tactical Overview'), + 'url' => Url::fromRequest() + ) + )->extend(new DashboardAction())->extend(new MenuAction())->activate('tactical_overview'); + + $stats = $this->backend->select()->from( + 'statussummary', + array( + 'hosts_up', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_pending', + 'hosts_pending_not_checked', + 'hosts_not_checked', + + 'services_ok', + 'services_warning_handled', + 'services_warning_unhandled', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_pending', + 'services_pending_not_checked', + 'services_not_checked', + ) + ); + $this->applyRestriction('monitoring/filter/objects', $stats); + + $this->setupFilterControl($stats, null, ['host', 'service'], ['format']); + $this->view->setHelperFunction('filteredUrl', function ($path, array $params) { + $filter = clone $this->view->filterEditor->getFilter(); + + return $this->view->url($path)->setParams($params)->addFilter($filter); + }); + + $this->handleFormatRequest($stats); + $summary = $stats->fetchRow(); + + // Correct pending counts. Done here instead of in the query for compatibility reasons. + $summary->hosts_pending -= $summary->hosts_pending_not_checked; + $summary->services_pending -= $summary->services_pending_not_checked; + + $hostSummaryChart = new Donut(); + $hostSummaryChart + ->addSlice($summary->hosts_up, array('class' => 'slice-state-ok')) + ->addSlice($summary->hosts_down_handled, array('class' => 'slice-state-critical-handled')) + ->addSlice($summary->hosts_down_unhandled, array('class' => 'slice-state-critical')) + ->addSlice($summary->hosts_unreachable_handled, array('class' => 'slice-state-unreachable-handled')) + ->addSlice($summary->hosts_unreachable_unhandled, array('class' => 'slice-state-unreachable')) + ->addSlice($summary->hosts_pending, array('class' => 'slice-state-pending')) + ->addSlice($summary->hosts_pending_not_checked, array('class' => 'slice-state-not-checked')) + ->setLabelBig($summary->hosts_down_unhandled) + ->setLabelBigEyeCatching($summary->hosts_down_unhandled > 0) + ->setLabelSmall($this->translate('Hosts Down')); + + $serviceSummaryChart = new Donut(); + $serviceSummaryChart + ->addSlice($summary->services_ok, array('class' => 'slice-state-ok')) + ->addSlice($summary->services_warning_handled, array('class' => 'slice-state-warning-handled')) + ->addSlice($summary->services_warning_unhandled, array('class' => 'slice-state-warning')) + ->addSlice($summary->services_critical_handled, array('class' => 'slice-state-critical-handled')) + ->addSlice($summary->services_critical_unhandled, array('class' => 'slice-state-critical')) + ->addSlice($summary->services_unknown_handled, array('class' => 'slice-state-unknown-handled')) + ->addSlice($summary->services_unknown_unhandled, array('class' => 'slice-state-unknown')) + ->addSlice($summary->services_pending, array('class' => 'slice-state-pending')) + ->addSlice($summary->services_pending_not_checked, array('class' => 'slice-state-not-checked')) + ->setLabelBig($summary->services_critical_unhandled ?: $summary->services_unknown_unhandled) + ->setLabelBigState($summary->services_critical_unhandled > 0 ? 'critical' : ( + $summary->services_unknown_unhandled > 0 ? 'unknown' : null + )) + ->setLabelSmall($summary->services_critical_unhandled > 0 || $summary->services_unknown_unhandled < 1 + ? $this->translate('Services Critical') + : $this->translate('Services Unknown')); + + $this->view->hostStatusSummaryChart = $hostSummaryChart + ->setLabelBigUrl($this->view->filteredUrl( + 'monitoring/list/hosts', + array( + 'host_state' => 1, + 'host_handled' => 0, + 'sort' => 'host_last_check', + 'dir' => 'asc' + ) + )) + ->render(); + $this->view->serviceStatusSummaryChart = $serviceSummaryChart + ->setLabelBigUrl($this->view->filteredUrl( + 'monitoring/list/services', + array( + 'service_state' => $summary->services_critical_unhandled > 0 + || ! $summary->services_unknown_unhandled ? 2 : 3, + 'service_handled' => 0, + 'sort' => 'service_last_check', + 'dir' => 'asc' + ) + )) + ->render(); + $this->view->statusSummary = $summary; + } +} diff --git a/modules/monitoring/application/controllers/TimelineController.php b/modules/monitoring/application/controllers/TimelineController.php new file mode 100644 index 0000000..deeeb36 --- /dev/null +++ b/modules/monitoring/application/controllers/TimelineController.php @@ -0,0 +1,325 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Controllers; + +use DateInterval; +use DateTime; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Timeline\TimeLine; +use Icinga\Module\Monitoring\Timeline\TimeRange; +use Icinga\Module\Monitoring\Web\Widget\SelectBox; +use Icinga\Util\Format; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +class TimelineController extends Controller +{ + public function indexAction() + { + $this->getTabs()->add( + 'timeline', + array( + 'title' => $this->translate('Show the number of historical event records grouped by time and type'), + 'label' => $this->translate('Timeline'), + 'url' => Url::fromRequest() + ) + )->extend(new DashboardAction())->extend(new MenuAction())->activate('timeline'); + $this->view->title = $this->translate('Timeline'); + + // TODO: filter for hard_states (precedence adjustments necessary!) + $this->setupIntervalBox(); + list($displayRange, $forecastRange) = $this->buildTimeRanges(); + + $detailUrl = Url::fromPath('monitoring/list/eventhistory'); + + $timeline = new TimeLine( + $this->applyRestriction( + 'monitoring/filter/objects', + $this->backend->select()->from( + 'eventhistory', + array( + 'name' => 'type', + 'time' => 'timestamp' + ) + ) + ), + array( + 'notification_ack' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'notification_flapping' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'notification_flapping_end' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'notification_dt_start' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'notification_dt_end' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'notification_custom' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'notification_state' => array( + 'class' => 'timeline-notification', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Notifications'), + 'groupBy' => 'notification_*' + ), + 'hard_state' => array( + 'class' => 'timeline-hard-state', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Hard state changes') + ), + 'comment' => array( + 'class' => 'timeline-comment', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Comments') + ), + 'ack' => array( + 'class' => 'timeline-ack', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Acknowledgements') + ), + 'dt_start' => array( + 'class' => 'timeline-downtime-start', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Started downtimes') + ), + 'dt_end' => array( + 'class' => 'timeline-downtime-end', + 'detailUrl' => $detailUrl, + 'label' => mt('monitoring', 'Ended downtimes') + ) + ) + ); + $timeline->setMaximumCircleWidth('6em'); + $timeline->setMinimumCircleWidth('0.3em'); + $timeline->setDisplayRange($displayRange); + $timeline->setForecastRange($forecastRange); + $beingExtended = $this->getRequest()->getParam('extend') == 1; + $timeline->setSession($this->Window()->getSessionNamespace('timeline', !$beingExtended)); + + $this->view->timeline = $timeline; + $this->view->nextRange = $forecastRange; + $this->view->beingExtended = $beingExtended; + $this->view->intervalFormat = $this->getIntervalFormat(); + $oldBase = $timeline->getCalculationBase(false); + $this->view->switchedContext = $oldBase !== null && $oldBase !== $timeline->getCalculationBase(true); + } + + /** + * Create a select box the user can choose the timeline interval from + */ + private function setupIntervalBox() + { + $box = new SelectBox( + 'intervalBox', + array( + '4h' => mt('monitoring', '4 Hours'), + '1d' => mt('monitoring', 'One day'), + '1w' => mt('monitoring', 'One week'), + '1m' => mt('monitoring', 'One month'), + '1y' => mt('monitoring', 'One year') + ), + mt('monitoring', 'TimeLine interval'), + 'interval' + ); + $box->applyRequest($this->getRequest()); + $this->view->intervalBox = $box; + } + + /** + * Return the chosen interval + * + * @return DateInterval The chosen interval + */ + private function getTimelineInterval() + { + switch ($this->view->intervalBox->getInterval()) { + case '1d': + return new DateInterval('P1D'); + case '1w': + return new DateInterval('P1W'); + case '1m': + return new DateInterval('P1M'); + case '1y': + return new DateInterval('P1Y'); + default: + return new DateInterval('PT4H'); + } + } + + /** + * Get an appropriate datetime format string for the chosen interval + * + * @return string + */ + private function getIntervalFormat() + { + switch ($this->view->intervalBox->getInterval()) { + case '1d': + return $this->getDateFormat(); + case '1w': + return '\W\e\ek W\<b\r\>\of Y'; + case '1m': + return 'F Y'; + case '1y': + return 'Y'; + default: + return $this->getDateFormat() . '\<b\r\>' . $this->getTimeFormat(); + } + } + + /** + * Return a preload interval based on the chosen timeline interval and the given date and time + * + * @param DateTime $dateTime The date and time to use + * + * @return DateInterval The interval to pre-load + */ + private function getPreloadInterval(DateTime $dateTime) + { + switch ($this->view->intervalBox->getInterval()) { + case '1d': + return DateInterval::createFromDateString('1 week -1 second'); + case '1w': + return DateInterval::createFromDateString('8 weeks -1 second'); + case '1m': + $dateCopy = clone $dateTime; + for ($i = 0; $i < 6; $i++) { + $dateCopy->sub(new DateInterval('PT' . Format::secondsByMonth($dateCopy) . 'S')); + } + return $dateCopy->add(new DateInterval('PT1S'))->diff($dateTime); + case '1y': + $dateCopy = clone $dateTime; + for ($i = 0; $i < 4; $i++) { + $dateCopy->sub(new DateInterval('PT' . Format::secondsByYear($dateCopy) . 'S')); + } + return $dateCopy->add(new DateInterval('PT1S'))->diff($dateTime); + default: + return DateInterval::createFromDateString('1 day -1 second'); + } + } + + /** + * Extrapolate the given datetime based on the chosen timeline interval + * + * @param DateTime $dateTime The datetime to extrapolate + */ + private function extrapolateDateTime(DateTime &$dateTime) + { + switch ($this->view->intervalBox->getInterval()) { + case '1d': + $dateTime->setTimestamp(strtotime('tomorrow', $dateTime->getTimestamp()) - 1); + break; + case '1w': + $dateTime->setTimestamp(strtotime('next monday', $dateTime->getTimestamp()) - 1); + break; + case '1m': + $dateTime->setTimestamp( + strtotime( + 'last day of this month', + strtotime( + 'tomorrow', + $dateTime->getTimestamp() + ) - 1 + ) + ); + break; + case '1y': + $dateTime->setTimestamp(strtotime('1 january next year', $dateTime->getTimestamp()) - 1); + break; + default: + $hour = $dateTime->format('G'); + $end = $hour < 4 ? 4 : ($hour < 8 ? 8 : ($hour < 12 ? 12 : ($hour < 16 ? 16 : ($hour < 20 ? 20 : 24)))); + $dateTime = DateTime::createFromFormat( + 'd/m/y G:i:s', + $dateTime->format('d/m/y') . ($end - 1) . ':59:59' + ); + } + } + + /** + * Return a display- and forecast time range + * + * Assembles a time range each for display and forecast purposes based on the start- and + * end time if given in the current request otherwise based on the current time and a + * end time that is calculated based on the chosen timeline interval. + * + * @return array The resulting time ranges + */ + private function buildTimeRanges() + { + $startTime = new DateTime(); + $startParam = $this->_request->getParam('start'); + $startTimestamp = is_numeric($startParam) ? intval($startParam) : strtotime($startParam ?? ''); + if ($startTimestamp !== false) { + $startTime->setTimestamp($startTimestamp); + } else { + $this->extrapolateDateTime($startTime); + } + + $endTime = clone $startTime; + $endParam = $this->_request->getParam('end'); + $endTimestamp = is_numeric($endParam) ? intval($endParam) : strtotime($endParam ?? ''); + if ($endTimestamp !== false) { + $endTime->setTimestamp($endTimestamp); + } else { + $endTime->sub($this->getPreloadInterval($startTime)); + } + + $forecastStart = clone $endTime; + $forecastStart->sub(new DateInterval('PT1S')); + $forecastEnd = clone $forecastStart; + $forecastEnd->sub($this->getPreloadInterval($forecastStart)); + + $timelineInterval = $this->getTimelineInterval(); + return array( + new TimeRange($startTime, $endTime, $timelineInterval), + new TimeRange($forecastStart, $forecastEnd, $timelineInterval) + ); + } + + /** + * Get the user's preferred time format or the application's default + * + * @return string + */ + private function getTimeFormat() + { + return 'H:i'; + } + + /** + * Get the user's preferred date format or the application's default + * + * @return string + */ + private function getDateFormat() + { + return 'Y-m-d'; + } +} diff --git a/modules/monitoring/application/forms/Command/CommandForm.php b/modules/monitoring/application/forms/Command/CommandForm.php new file mode 100644 index 0000000..34391cf --- /dev/null +++ b/modules/monitoring/application/forms/Command/CommandForm.php @@ -0,0 +1,92 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command; + +use Icinga\Exception\ConfigurationError; +use Icinga\Web\Form; +use Icinga\Web\Request; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Command\Transport\CommandTransport; +use Icinga\Module\Monitoring\Command\Transport\CommandTransportInterface; + +/** + * Base class for command forms + */ +abstract class CommandForm extends Form +{ + /** + * Monitoring backend + * + * @var MonitoringBackend + */ + protected $backend; + + /** + * Set the monitoring backend + * + * @param MonitoringBackend $backend + * + * @return $this + */ + public function setBackend(MonitoringBackend $backend) + { + $this->backend = $backend; + return $this; + } + + /** + * Get the monitoring backend + * + * @return MonitoringBackend + */ + public function getBackend() + { + return $this->backend; + } + + /** + * Get the transport used to send commands + * + * @param Request $request + * + * @return CommandTransportInterface + * + * @throws ConfigurationError + */ + public function getTransport(Request $request) + { + if (($transportName = $request->getParam('transport')) !== null) { + $config = CommandTransport::getConfig(); + if ($config->hasSection($transportName)) { + $transport = CommandTransport::createTransport($config->getSection($transportName)); + } else { + throw new ConfigurationError(sprintf( + mt('monitoring', 'Command transport "%s" not found.'), + $transportName + )); + } + } else { + $transport = new CommandTransport(); + } + + return $transport; + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() + { + $redirectUrl = parent::getRedirectUrl(); + // TODO(el): Forms should provide event handling. This is quite hackish + $formData = $this->getRequestData(); + if ($this->wasSent($formData) + && (! $this->getSubmitLabel() || $this->isSubmitted()) + && $this->isValid($formData) + ) { + $this->getResponse()->setAutoRefreshInterval(1); + } + return $redirectUrl; + } +} diff --git a/modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php b/modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php new file mode 100644 index 0000000..ee49962 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php @@ -0,0 +1,64 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Instance; + +use DateTime; +use DateInterval; +use Icinga\Module\Monitoring\Command\Instance\DisableNotificationsExpireCommand; +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Web\Notification; + +/** + * Form for disabling host and service notifications w/ an optional expire date and time on an Icinga instance + */ +class DisableNotificationsExpireCommandForm extends CommandForm +{ + /** + * (non-PHPDoc) + * @see \Zend_Form::init() For the method documentation. + */ + public function init() + { + $this->setRequiredCue(null); + $this->setSubmitLabel($this->translate('Disable Notifications')); + $this->addDescription($this->translate( + 'This command is used to disable host and service notifications for a specific time.' + )); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval('PT1H')); + $this->addElement( + 'dateTimePicker', + 'expire_time', + array( + 'required' => true, + 'label' => $this->translate('Expire Time'), + 'description' => $this->translate('Set the expire time.'), + 'value' => $expireTime + ) + ); + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + $disableNotifications = new DisableNotificationsExpireCommand(); + $disableNotifications + ->setExpireTime($this->getElement('expire_time')->getValue()->getTimestamp()); + $this->getTransport($this->request)->send($disableNotifications); + Notification::success($this->translate('Disabling host and service notifications..')); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php b/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php new file mode 100644 index 0000000..8b01399 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php @@ -0,0 +1,279 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Instance; + +use Icinga\Module\Monitoring\Command\Instance\ToggleInstanceFeatureCommand; +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Web\Notification; + +/** + * Form for enabling or disabling features of Icinga instances + */ +class ToggleInstanceFeaturesCommandForm extends CommandForm +{ + /** + * Instance status + * + * @var object + */ + protected $status; + + /** + * (non-PHPDoc) + * @see \Zend_Form::init() For the method documentation. + */ + public function init() + { + $this->setUseFormAutosubmit(); + $this->setAttrib('class', self::DEFAULT_CLASSES . ' instance-features'); + } + + /** + * Set the instance status + * + * @param object $status + * + * @return $this + */ + public function setStatus($status) + { + $this->status = (object) $status; + return $this; + } + + /** + * Get the instance status + * + * @return object + */ + public function getStatus() + { + return $this->status; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $notificationDescription = null; + $isIcinga2 = $this->getBackend()->isIcinga2($this->status->program_version); + + if (! $isIcinga2) { + if ((bool) $this->status->notifications_enabled) { + if ($this->hasPermission('monitoring/command/feature/instance')) { + $notificationDescription = sprintf( + '<a aria-label="%1$s" class="action-link" title="%1$s"' + . ' href="%2$s" data-base-target="_next">%3$s</a>', + $this->translate('Disable notifications for a specific time on a program-wide basis'), + $this->getView()->href('monitoring/health/disable-notifications'), + $this->translate('Disable temporarily') + ); + } else { + $notificationDescription = null; + } + } elseif ($this->status->disable_notif_expire_time) { + $notificationDescription = sprintf( + $this->translate('Notifications will be re-enabled in <strong>%s</strong>'), + $this->getView()->timeUntil($this->status->disable_notif_expire_time) + ); + } + } + + $toggleDisabled = $this->hasPermission('monitoring/command/feature/instance') ? null : ''; + + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS, + array( + 'label' => $this->translate('Active Host Checks'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS, + array( + 'label' => $this->translate('Active Service Checks'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS, + array( + 'label' => $this->translate('Event Handlers'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION, + array( + 'label' => $this->translate('Flap Detection'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS, + array( + 'label' => $this->translate('Notifications'), + 'autosubmit' => true, + 'description' => $notificationDescription, + 'decorators' => array( + array('Label', array('tag'=>'span', 'separator' => '', 'class' => 'control-label')), + array( + 'Description', + array('tag' => 'span', 'class' => 'description', 'escape' => false) + ), + array(array('labelWrap' => 'HtmlTag'), array('tag' => 'div', 'class' => 'control-label-group')), + array('ViewHelper', array('separator' => '')), + array('Errors', array('separator' => '')), + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group')) + ), + 'disabled' => $toggleDisabled + ) + ); + + if (! $isIcinga2) { + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING, + array( + 'label' => $this->translate('Obsessing Over Hosts'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING, + array( + 'label' => $this->translate('Obsessing Over Services'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS, + array( + 'label' => $this->translate('Passive Host Checks'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS, + array( + 'label' => $this->translate('Passive Service Checks'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + } + + $this->addElement( + 'checkbox', + ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA, + array( + 'label' => $this->translate('Performance Data'), + 'autosubmit' => true, + 'disabled' => $toggleDisabled + ) + ); + } + + /** + * Load feature status + * + * @param object $instanceStatus + * + * @return $this + */ + public function load($instanceStatus) + { + $this->create(); + foreach ($this->getValues() as $feature => $enabled) { + $this->getElement($feature)->setChecked($instanceStatus->{$feature}); + } + + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + $this->assertPermission('monitoring/command/feature/instance'); + + $notifications = array( + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS => array( + $this->translate('Enabling active host checks..'), + $this->translate('Disabling active host checks..') + ), + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS => array( + $this->translate('Enabling active service checks..'), + $this->translate('Disabling active service checks..') + ), + ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS => array( + $this->translate('Enabling event handlers..'), + $this->translate('Disabling event handlers..') + ), + ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION => array( + $this->translate('Enabling flap detection..'), + $this->translate('Disabling flap detection..') + ), + ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS => array( + $this->translate('Enabling notifications..'), + $this->translate('Disabling notifications..') + ), + ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING => array( + $this->translate('Enabling obsessing over hosts..'), + $this->translate('Disabling obsessing over hosts..') + ), + ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING => array( + $this->translate('Enabling obsessing over services..'), + $this->translate('Disabling obsessing over services..') + ), + ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS => array( + $this->translate('Enabling passive host checks..'), + $this->translate('Disabling passive host checks..') + ), + ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS => array( + $this->translate('Enabling passive service checks..'), + $this->translate('Disabling passive service checks..') + ), + ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA => array( + $this->translate('Enabling performance data..'), + $this->translate('Disabling performance data..') + ) + ); + + foreach ($this->getValues() as $feature => $enabled) { + if ((bool) $this->status->{$feature} !== (bool) $enabled) { + $toggleFeature = new ToggleInstanceFeatureCommand(); + $toggleFeature + ->setFeature($feature) + ->setEnabled($enabled); + $this->getTransport($this->request)->send($toggleFeature); + + Notification::success( + $notifications[$feature][$enabled ? 0 : 1] + ); + } + } + + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php b/modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php new file mode 100644 index 0000000..c7caf5d --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php @@ -0,0 +1,172 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use DateTime; +use DateInterval; +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Command\Object\AcknowledgeProblemCommand; +use Icinga\Web\Notification; + +/** + * Form for acknowledging host or service problems + */ +class AcknowledgeProblemCommandForm extends ObjectsCommandForm +{ + /** + * Initialize this form + */ + public function init() + { + $this->addDescription($this->translate( + 'This command is used to acknowledge host or service problems. When a problem is acknowledged,' + . ' future notifications about problems are temporarily disabled until the host or service' + . ' recovers.' + )); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation. + */ + public function getSubmitLabel() + { + return $this->translatePlural('Acknowledge problem', 'Acknowledge problems', count($this->objects)); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $config = Config::module('monitoring'); + + $acknowledgeExpire = (bool) $config->get('settings', 'acknowledge_expire', false); + + $this->addElements(array( + array( + 'textarea', + 'comment', + array( + 'required' => true, + 'label' => $this->translate('Comment'), + 'description' => $this->translate( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ), + 'attribs' => array('class' => 'autofocus') + ) + ), + array( + 'checkbox', + 'persistent', + array( + 'label' => $this->translate('Persistent Comment'), + 'value' => (bool) $config->get('settings', 'acknowledge_persistent', false), + 'description' => $this->translate( + 'If you would like the comment to remain even when the acknowledgement is removed, check this' + . ' option.' + ) + ) + ), + array( + 'checkbox', + 'expire', + array( + 'label' => $this->translate('Use Expire Time'), + 'value' => $acknowledgeExpire, + 'description' => $this->translate( + 'If the acknowledgement should expire, check this option.' + ), + 'autosubmit' => true + ) + ) + )); + $expire = isset($formData['expire']) ? $formData['expire'] : $acknowledgeExpire; + if ($expire) { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval($config->get('settings', 'acknowledge_expire_time', 'PT1H'))); + $this->addElement( + 'dateTimePicker', + 'expire_time', + array( + 'label' => $this->translate('Expire Time'), + 'value' => $expireTime, + 'description' => $this->translate( + 'Enter the expire date and time for this acknowledgement here. Icinga will delete the' + . ' acknowledgement after this time expired.' + ) + ) + ); + $this->addDisplayGroup( + array('expire', 'expire_time'), + 'expire-expire_time', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div')) + ) + ) + ); + } + $this->addElements(array( + array( + 'checkbox', + 'sticky', + array( + 'label' => $this->translate('Sticky Acknowledgement'), + 'value' => (bool) $config->get('settings', 'acknowledge_sticky', false), + 'description' => $this->translate( + 'If you want the acknowledgement to remain until the host or service recovers even if the host' + . ' or service changes state, check this option.' + ) + ) + ), + array( + 'checkbox', + 'notify', + array( + 'label' => $this->translate('Send Notification'), + 'value' => (bool) $config->get('settings', 'acknowledge_notify', true), + 'description' => $this->translate( + 'If you do not want an acknowledgement notification to be sent out to the appropriate contacts,' + . ' uncheck this option.' + ) + ) + ) + )); + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + $ack = new AcknowledgeProblemCommand(); + $ack + ->setObject($object) + ->setComment($this->getElement('comment')->getValue()) + ->setAuthor($this->request->getUser()->getUsername()) + ->setPersistent($this->getElement('persistent')->isChecked()) + ->setSticky($this->getElement('sticky')->isChecked()) + ->setNotify($this->getElement('notify')->isChecked()); + if ($this->getElement('expire')->isChecked()) { + $ack->setExpireTime($this->getElement('expire_time')->getValue()->getTimestamp()); + } + $this->getTransport($this->request)->send($ack); + } + Notification::success($this->translatePlural( + 'Acknowledging problem..', + 'Acknowledging problems..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php b/modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php new file mode 100644 index 0000000..72133a0 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php @@ -0,0 +1,148 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Command\Object\AddCommentCommand; +use Icinga\Web\Notification; + +/** + * Form for adding host or service comments + */ +class AddCommentCommandForm extends ObjectsCommandForm +{ + /** + * Initialize this form + */ + public function init() + { + $this->addDescription($this->translate('This command is used to add host or service comments.')); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation. + */ + public function getSubmitLabel() + { + return $this->translatePlural('Add comment', 'Add comments', count($this->objects)); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $this->addElement( + 'textarea', + 'comment', + array( + 'required' => true, + 'label' => $this->translate('Comment'), + 'description' => $this->translate( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ), + 'attribs' => array('class' => 'autofocus') + ) + ); + if (! $this->getBackend()->isIcinga2()) { + $this->addElement( + 'checkbox', + 'persistent', + array( + 'label' => $this->translate('Persistent'), + 'value' => (bool) Config::module('monitoring')->get('settings', 'comment_persistent', true), + 'description' => $this->translate( + 'If you uncheck this option, the comment will automatically be deleted the next time Icinga is' + . ' restarted.' + ) + ) + ); + } + + if (version_compare($this->getBackend()->getProgramVersion(), '2.13.0', '>=')) { + $config = Config::module('monitoring'); + $commentExpire = (bool) $config->get('settings', 'comment_expire', false); + + $this->addElement( + 'checkbox', + 'expire', + [ + 'label' => $this->translate('Use Expire Time'), + 'value' => $commentExpire, + 'description' => $this->translate('If the comment should expire, check this option.'), + 'autosubmit' => true + ] + ); + + if (isset($formData['expire']) ? $formData['expire'] : $commentExpire) { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval($config->get('settings', 'comment_expire_time', 'PT1H'))); + + $this->addElement( + 'dateTimePicker', + 'expire_time', + [ + 'label' => $this->translate('Expire Time'), + 'value' => $expireTime, + 'description' => $this->translate( + 'Enter the expire date and time for this comment here. Icinga will delete the' + . ' comment after this time expired.' + ) + ] + ); + + $this->addDisplayGroup( + ['expire', 'expire_time'], + 'expire-expire_time', + [ + 'decorators' => [ + 'FormElements', + ['HtmlTag', ['tag' => 'div']] + ] + ] + ); + } + } + + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + $comment = new AddCommentCommand(); + $comment->setObject($object); + $comment->setComment($this->getElement('comment')->getValue()); + $comment->setAuthor($this->request->getUser()->getUsername()); + if (($persistent = $this->getElement('persistent')) !== null) { + $comment->setPersistent($persistent->isChecked()); + } + + $expire = $this->getElement('expire'); + + if ($expire !== null && $expire->isChecked()) { + $comment->setExpireTime($this->getElement('expire_time')->getValue()->getTimestamp()); + } + + $this->getTransport($this->request)->send($comment); + } + Notification::success($this->translatePlural( + 'Adding comment..', + 'Adding comments..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php b/modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php new file mode 100644 index 0000000..a586d2f --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php @@ -0,0 +1,87 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\ScheduleHostCheckCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand; +use Icinga\Web\Notification; + +/** + * Form for immediately checking hosts or services + */ +class CheckNowCommandForm extends ObjectsCommandForm +{ + /** + * (non-PHPDoc) + * @see \Zend_Form::init() For the method documentation. + */ + public function init() + { + $this->setAttrib('class', 'inline'); + $this->setSubmitLabel($this->translate('Check now')); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::addSubmitButton() For the method documentation. + */ + public function addSubmitButton() + { + $this->addElements(array( + array( + 'button', + 'btn_submit', + array( + 'class' => 'link-button spinner', + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ), + 'escape' => false, + 'ignore' => true, + 'label' => $this->getView()->icon('arrows-cw') . $this->translate('Check now'), + 'type' => 'submit', + 'title' => $this->translate('Schedule the next active check to run immediately'), + 'value' => $this->translate('Check now') + ) + ) + )); + + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + if (! $object->active_checks_enabled + && ! $this->Auth()->hasPermission('monitoring/command/schedule-check') + ) { + continue; + } + + if ($object->getType() === $object::TYPE_HOST) { + $check = new ScheduleHostCheckCommand(); + } else { + $check = new ScheduleServiceCheckCommand(); + } + $check + ->setObject($object) + ->setForced() + ->setCheckTime(time()); + $this->getTransport($this->request)->send($check); + } + Notification::success(mtp( + 'monitoring', + 'Scheduling check..', + 'Scheduling checks..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php new file mode 100644 index 0000000..cd15b19 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php @@ -0,0 +1,109 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand; +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Web\Notification; + +/** + * Form for deleting host or service comments + */ +class DeleteCommentCommandForm extends CommandForm +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline'); + } + + /** + * {@inheritdoc} + */ + public function addSubmitButton() + { + $this->addElement( + 'button', + 'btn_submit', + array( + 'class' => 'link-button spinner', + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ), + 'escape' => false, + 'ignore' => true, + 'label' => $this->getView()->icon('cancel'), + 'title' => $this->translate('Delete this comment'), + 'type' => 'submit' + ) + ); + return $this; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + $this->addElements( + array( + array( + 'hidden', + 'comment_id', + array( + 'required' => true, + 'validators' => array('NotEmpty'), + 'decorators' => array('ViewHelper') + ) + ), + array( + 'hidden', + 'comment_is_service', + array( + 'filters' => array('Boolean'), + 'decorators' => array('ViewHelper') + ) + ), + array( + 'hidden', + 'comment_name', + array( + 'decorators' => array('ViewHelper') + ) + ), + array( + 'hidden', + 'redirect', + array( + 'decorators' => array('ViewHelper') + ) + ) + ) + ); + return $this; + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + $cmd = new DeleteCommentCommand(); + $cmd + ->setAuthor($this->Auth()->getUser()->getUsername()) + ->setCommentId($this->getElement('comment_id')->getValue()) + ->setCommentName($this->getElement('comment_name')->getValue()) + ->setIsService($this->getElement('comment_is_service')->getValue()); + $this->getTransport($this->request)->send($cmd); + $redirect = $this->getElement('redirect')->getValue(); + if (! empty($redirect)) { + $this->setRedirectUrl($redirect); + } + Notification::success($this->translate('Deleting comment..')); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php new file mode 100644 index 0000000..70ea7b8 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php @@ -0,0 +1,89 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand; +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Web\Notification; + +/** + * Form for deleting host or service comments + */ +class DeleteCommentsCommandForm extends CommandForm +{ + /** + * The comments to delete + * + * @var array + */ + protected $comments; + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline'); + } + + /** + * Set the comments to delete + * + * @param iterable $comments + * + * @return $this + */ + public function setComments($comments) + { + $this->comments = $comments; + return $this; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + $this->addElements(array( + array( + 'hidden', + 'redirect', + array('decorators' => array('ViewHelper')) + ) + )); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSubmitLabel() + { + return $this->translatePlural('Remove', 'Remove All', count($this->comments)); + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + foreach ($this->comments as $comment) { + $cmd = new DeleteCommentCommand(); + $cmd + ->setCommentId($comment->id) + ->setCommentName($comment->name) + ->setAuthor($this->Auth()->getUser()->getUsername()) + ->setIsService(isset($comment->service_description)); + $this->getTransport($this->request)->send($cmd); + } + $redirect = $this->getElement('redirect')->getValue(); + if (! empty($redirect)) { + $this->setRedirectUrl($redirect); + } + Notification::success( + $this->translatePlural('Deleting comment..', 'Deleting comments..', count($this->comments)) + ); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php new file mode 100644 index 0000000..79700cb --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php @@ -0,0 +1,129 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand; +use Icinga\Module\Monitoring\Exception\CommandTransportException; +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Web\Notification; + +/** + * Form for deleting host or service downtimes + */ +class DeleteDowntimeCommandForm extends CommandForm +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline'); + } + + /** + * {@inheritdoc} + */ + public function addSubmitButton() + { + $this->addElement( + 'button', + 'btn_submit', + array( + 'class' => 'link-button spinner', + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ), + 'escape' => false, + 'ignore' => true, + 'label' => $this->getView()->icon('cancel'), + 'title' => $this->translate('Delete this downtime'), + 'type' => 'submit' + ) + ); + return $this; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + $this->addElements( + array( + array( + 'hidden', + 'downtime_id', + array( + 'decorators' => array('ViewHelper'), + 'required' => true, + 'validators' => array('NotEmpty') + ) + ), + array( + 'hidden', + 'downtime_is_service', + array( + 'decorators' => array('ViewHelper'), + 'filters' => array('Boolean') + ) + ), + array( + 'hidden', + 'downtime_name', + array( + 'decorators' => array('ViewHelper') + ) + ), + array( + 'hidden', + 'redirect', + array( + 'decorators' => array('ViewHelper') + ) + ) + ) + ); + return $this; + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + $cmd = new DeleteDowntimeCommand(); + $cmd + ->setAuthor($this->Auth()->getUser()->getUsername()) + ->setDowntimeId($this->getElement('downtime_id')->getValue()) + ->setDowntimeName($this->getElement('downtime_name')->getValue()) + ->setIsService($this->getElement('downtime_is_service')->getValue()); + + $errorMsg = null; + + try { + $this->getTransport($this->request)->send($cmd); + } catch (CommandTransportException $e) { + $errorMsg = $e->getMessage(); + } + + if (! $errorMsg) { + $redirect = $this->getElement('redirect')->getValue(); + Notification::success($this->translate('Deleting downtime.')); + } else { + if (! $this->getIsApiTarget()) { + $redirect = $this->getRequest()->getUrl(); + } + + Notification::error($errorMsg); + } + + if (! empty($redirect)) { + $this->setRedirectUrl($redirect); + return true; + } + + return false; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php new file mode 100644 index 0000000..d4ee803 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php @@ -0,0 +1,89 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand; +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Web\Notification; + +/** + * Form for deleting host or service downtimes + */ +class DeleteDowntimesCommandForm extends CommandForm +{ + /** + * The downtimes to delete + * + * @var array + */ + protected $downtimes; + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline'); + } + + /** + * Set the downtimes to delete + * + * @param iterable $downtimes + * + * @return $this + */ + public function setDowntimes($downtimes) + { + $this->downtimes = $downtimes; + return $this; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + $this->addElements(array( + array( + 'hidden', + 'redirect', + array('decorators' => array('ViewHelper')) + ) + )); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSubmitLabel() + { + return $this->translatePlural('Remove', 'Remove All', count($this->downtimes)); + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + foreach ($this->downtimes as $downtime) { + $delDowntime = new DeleteDowntimeCommand(); + $delDowntime + ->setDowntimeId($downtime->id) + ->setDowntimeName($downtime->name) + ->setAuthor($this->Auth()->getUser()->getUsername()) + ->setIsService(isset($downtime->service_description)); + $this->getTransport($this->request)->send($delDowntime); + } + $redirect = $this->getElement('redirect')->getValue(); + if (! empty($redirect)) { + $this->setRedirectUrl($redirect); + } + Notification::success( + $this->translatePlural('Deleting downtime..', 'Deleting downtimes..', count($this->downtimes)) + ); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php b/modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php new file mode 100644 index 0000000..928c365 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php @@ -0,0 +1,47 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Forms\Command\CommandForm; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +/** + * Base class for Icinga object command forms + */ +abstract class ObjectsCommandForm extends CommandForm +{ + /** + * Involved Icinga objects + * + * @var array|\Traversable|\ArrayAccess + */ + protected $objects; + + /** + * Set the involved Icinga objects + * + * @param $objects MonitoredObject|array|\Traversable|\ArrayAccess + * + * @return $this + */ + public function setObjects($objects) + { + if ($objects instanceof MonitoredObject) { + $this->objects = array($objects); + } else { + $this->objects = $objects; + } + return $this; + } + + /** + * Get the involved Icinga objects + * + * @return array|\ArrayAccess|\Traversable + */ + public function getObjects() + { + return $this->objects; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php b/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php new file mode 100644 index 0000000..ab46071 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php @@ -0,0 +1,139 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Web\Notification; +use Icinga\Module\Monitoring\Command\Object\ProcessCheckResultCommand; + +/** + * Form for submitting a passive host or service check result + */ +class ProcessCheckResultCommandForm extends ObjectsCommandForm +{ + /** + * Initialize this form + */ + public function init() + { + $this->addDescription($this->translate( + 'This command is used to submit passive host or service check results.' + )); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation. + */ + public function getSubmitLabel() + { + return $this->translatePlural( + 'Submit Passive Check Result', + 'Submit Passive Check Results', + count($this->objects) + ); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData) + { + foreach ($this->getObjects() as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + // Nasty, but as getObjects() returns everything but an object with a real + // iterator interface this is the only way to fetch just the first element + break; + } + + $this->addElement( + 'select', + 'status', + array( + 'required' => true, + 'label' => $this->translate('Status'), + 'description' => $this->translate('The state this check result should report'), + 'multiOptions' => $object->getType() === $object::TYPE_HOST ? $this->getHostMultiOptions() : array( + ProcessCheckResultCommand::SERVICE_OK => $this->translate('OK', 'icinga.state'), + ProcessCheckResultCommand::SERVICE_WARNING => $this->translate('WARNING', 'icinga.state'), + ProcessCheckResultCommand::SERVICE_CRITICAL => $this->translate('CRITICAL', 'icinga.state'), + ProcessCheckResultCommand::SERVICE_UNKNOWN => $this->translate('UNKNOWN', 'icinga.state') + ) + ) + ); + $this->addElement( + 'text', + 'output', + array( + 'required' => true, + 'label' => $this->translate('Output'), + 'description' => $this->translate('The plugin output of this check result') + ) + ); + $this->addElement( + 'text', + 'perfdata', + array( + 'allowEmpty' => true, + 'label' => $this->translate('Performance Data'), + 'description' => $this->translate( + 'The performance data of this check result. Leave empty' + . ' if this check result has no performance data' + ) + ) + ); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + if (! $object->passive_checks_enabled) { + continue; + } + + $command = new ProcessCheckResultCommand(); + $command->setObject($object); + $command->setStatus($this->getValue('status')); + $command->setOutput($this->getValue('output')); + + if ($perfdata = $this->getValue('perfdata')) { + $command->setPerformanceData($perfdata); + } + + $this->getTransport($this->request)->send($command); + } + + Notification::success($this->translatePlural( + 'Processing check result..', + 'Processing check results..', + count($this->objects) + )); + + return true; + } + + /** + * Returns the available host options based on the program version + * + * @return array + */ + protected function getHostMultiOptions() + { + $options = array( + ProcessCheckResultCommand::HOST_UP => $this->translate('UP', 'icinga.state'), + ProcessCheckResultCommand::HOST_DOWN => $this->translate('DOWN', 'icinga.state') + ); + + if (! $this->getBackend()->isIcinga2()) { + $options[ProcessCheckResultCommand::HOST_UNREACHABLE] = $this->translate('UNREACHABLE', 'icinga.state'); + } + + return $options; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php b/modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php new file mode 100644 index 0000000..e45a055 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php @@ -0,0 +1,122 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\RemoveAcknowledgementCommand; +use Icinga\Web\Notification; + +/** + * Form for removing host or service problem acknowledgements + */ +class RemoveAcknowledgementCommandForm extends ObjectsCommandForm +{ + /** + * Whether to show the submit label next to the remove icon + * + * The submit label is disabled in detail views but should be enabled in multi-select views. + * + * @var bool + */ + protected $labelEnabled = false; + + /** + * Whether to show the submit label next to the remove icon + * + * @return bool + */ + public function isLabelEnabled() + { + return $this->labelEnabled; + } + + /** + * Set whether to show the submit label next to the remove icon + * + * @param bool $labelEnabled + * + * @return $this + */ + public function setLabelEnabled($labelEnabled) + { + $this->labelEnabled = (bool) $labelEnabled; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline'); + } + + /** + * {@inheritdoc} + */ + public function addSubmitButton() + { + $this->addElement( + 'button', + 'btn_submit', + array( + 'class' => 'link-button spinner', + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ), + 'escape' => false, + 'ignore' => true, + 'label' => $this->getSubmitLabel(), + 'title' => $this->translatePlural( + 'Remove acknowledgement', + 'Remove acknowledgements', + count($this->objects) + ), + 'type' => 'submit' + ) + ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSubmitLabel() + { + $label = $this->getView()->icon('cancel'); + if ($this->isLabelEnabled()) { + $label .= $this->translatePlural( + 'Remove acknowledgement', + 'Remove acknowledgements', + count($this->objects) + ); + } + + return $label; + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + $removeAck = new RemoveAcknowledgementCommand(); + $removeAck->setObject($object); + $removeAck->setAuthor($this->Auth()->getUser()->getUsername()); + $this->getTransport($this->request)->send($removeAck); + } + Notification::success(mtp( + 'monitoring', + 'Removing acknowledgement..', + 'Removing acknowledgements..', + count($this->objects) + )); + + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php new file mode 100644 index 0000000..55b044f --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php @@ -0,0 +1,67 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Command\Object\ScheduleHostCheckCommand; +use Icinga\Web\Notification; + +/** + * Form for scheduling host checks + */ +class ScheduleHostCheckCommandForm extends ScheduleServiceCheckCommandForm +{ + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $config = Config::module('monitoring'); + + parent::createElements($formData); + $this->addElements(array( + array( + 'checkbox', + 'all_services', + array( + 'label' => $this->translate('All Services'), + 'value' => (bool) $config->get('settings', 'hostcheck_all_services', false), + 'description' => $this->translate( + 'Schedule check for all services on the hosts and the hosts themselves.' + ) + ) + ) + )); + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + if (! $object->active_checks_enabled + && ! $this->Auth()->hasPermission('monitoring/command/schedule-check') + ) { + continue; + } + + $check = new ScheduleHostCheckCommand(); + $check + ->setObject($object) + ->setOfAllServices($this->getElement('all_services')->isChecked()); + $this->scheduleCheck($check, $this->request); + } + Notification::success($this->translatePlural( + 'Scheduling host check..', + 'Scheduling host checks..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php new file mode 100644 index 0000000..89db1ce --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php @@ -0,0 +1,178 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Command\Object\ApiScheduleHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand; +use Icinga\Module\Monitoring\Command\Transport\ApiCommandTransport; +use Icinga\Module\Monitoring\Command\Transport\CommandTransport; +use Icinga\Web\Notification; + +/** + * Form for scheduling host downtimes + */ +class ScheduleHostDowntimeCommandForm extends ScheduleServiceDowntimeCommandForm +{ + /** @var bool */ + protected $hostDowntimeAllServices; + + public function init() + { + $this->start = new DateTime(); + $config = Config::module('monitoring'); + $this->commentText = $config->get('settings', 'hostdowntime_comment_text'); + + $this->hostDowntimeAllServices = (bool) $config->get('settings', 'hostdowntime_all_services', false); + + $fixedEnd = clone $this->start; + $fixed = $config->get('settings', 'hostdowntime_end_fixed', 'PT1H'); + $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed)); + + $flexibleEnd = clone $this->start; + $flexible = $config->get('settings', 'hostdowntime_end_flexible', 'PT1H'); + $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible)); + + $flexibleDuration = $config->get('settings', 'hostdowntime_flexible_duration', 'PT2H'); + $this->flexibleDuration = new DateInterval($flexibleDuration); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + parent::createElements($formData); + + $this->addElement( + 'checkbox', + 'all_services', + array( + 'description' => $this->translate( + 'Schedule downtime for all services on the hosts and the hosts themselves.' + ), + 'label' => $this->translate('All Services'), + 'value' => $this->hostDowntimeAllServices + ) + ); + + if (! $this->getBackend()->isIcinga2() + || version_compare($this->getBackend()->getProgramVersion(), '2.6.0', '>=') + ) { + $this->addElement( + 'select', + 'child_hosts', + array( + 'description' => $this->translate( + 'Define what should be done with the child hosts of the hosts.' + ), + 'label' => $this->translate('Child Hosts'), + 'multiOptions' => array( + 0 => $this->translate('Do nothing with child hosts'), + 1 => $this->translate('Schedule triggered downtime for all child hosts'), + 2 => $this->translate('Schedule non-triggered downtime for all child hosts') + ), + 'value' => 0 + ) + ); + } + + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + $end = $this->getValue('end')->getTimestamp(); + if ($end <= $this->getValue('start')->getTimestamp()) { + $endElement = $this->_elements['end']; + $endElement->setValue($endElement->getValue()->format($endElement->getFormat())); + $endElement->addError($this->translate('The end time must be greater than the start time')); + return false; + } + + $now = new DateTime; + if ($end <= $now->getTimestamp()) { + $endElement = $this->_elements['end']; + $endElement->setValue($endElement->getValue()->format($endElement->getFormat())); + $endElement->addError($this->translate('A downtime must not be in the past')); + return false; + } + + // Send all_services API parameter if Icinga is equal to or greater than 2.11.0 + $allServicesNative = version_compare($this->getBackend()->getProgramVersion(), '2.11.0', '>='); + // Use ApiScheduleHostDowntimeCommand only when Icinga is equal to or greater than 2.11.0 and + // when an API command transport is requested or only API command transports are configured: + $useApiDowntime = $allServicesNative; + if ($useApiDowntime) { + $transport = $this->getTransport($this->getRequest()); + if ($transport instanceof CommandTransport) { + foreach ($transport::getConfig() as $config) { + if (strtolower($config->transport) !== 'api') { + $useApiDowntime = false; + break; + } + } + } elseif (! $transport instanceof ApiCommandTransport) { + $useApiDowntime = false; + } + } + + foreach ($this->objects as $object) { + if ($useApiDowntime) { + $hostDowntime = (new ApiScheduleHostDowntimeCommand()) + ->setForAllServices($this->getElement('all_services')->isChecked()) + ->setChildOptions((int) $this->getElement('child_hosts')->getValue()); + // Code duplicated for readability and scope + $hostDowntime->setObject($object); + $this->scheduleDowntime($hostDowntime, $this->request); + + continue; + } + + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + if (($childHostsEl = $this->getElement('child_hosts')) !== null) { + $childHosts = (int) $childHostsEl->getValue(); + } else { + $childHosts = 0; + } + $allServices = $this->getElement('all_services')->isChecked(); + if ($childHosts === 0) { + $hostDowntime = (new ScheduleHostDowntimeCommand()) + ->setForAllServicesNative($allServicesNative); + if ($allServices === true) { + $hostDowntime->setForAllServices(); + }; + } else { + $hostDowntime = new PropagateHostDowntimeCommand(); + if ($childHosts === 1) { + $hostDowntime->setTriggered(); + } + if ($allServices === true) { + foreach ($object->services as $service) { + $serviceDowntime = new ScheduleServiceDowntimeCommand(); + $serviceDowntime->setObject($service); + $this->scheduleDowntime($serviceDowntime, $this->request); + } + } + } + $hostDowntime->setObject($object); + $this->scheduleDowntime($hostDowntime, $this->request); + } + Notification::success($this->translatePlural( + 'Scheduling host downtime..', + 'Scheduling host downtimes..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php new file mode 100644 index 0000000..f65aea8 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php @@ -0,0 +1,112 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use DateTime; +use DateInterval; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand; +use Icinga\Web\Notification; +use Icinga\Web\Request; + +/** + * Form for scheduling service checks + */ +class ScheduleServiceCheckCommandForm extends ObjectsCommandForm +{ + /** + * Initialize this form + */ + public function init() + { + $this->addDescription($this->translate( + 'This command is used to schedule the next check of hosts or services. Icinga will re-queue the' + . ' hosts or services to be checked at the time you specify.' + )); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation. + */ + public function getSubmitLabel() + { + return $this->translatePlural('Schedule check', 'Schedule checks', count($this->objects)); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $checkTime = new DateTime(); + $checkTime->add(new DateInterval('PT1H')); + $this->addElements(array( + array( + 'dateTimePicker', + 'check_time', + array( + 'required' => true, + 'label' => $this->translate('Check Time'), + 'description' => $this->translate( + 'Set the date and time when the check should be scheduled.' + ), + 'value' => $checkTime + ) + ), + array( + 'checkbox', + 'force_check', + array( + 'label' => $this->translate('Force Check'), + 'description' => $this->translate( + 'If you select this option, Icinga will force a check regardless of both what time the' + . ' scheduled check occurs and whether or not checks are enabled.' + ) + ) + ) + )); + return $this; + } + + /** + * Schedule a check + * + * @param ScheduleServiceCheckCommand $check + * @param Request $request + */ + public function scheduleCheck(ScheduleServiceCheckCommand $check, Request $request) + { + $check + ->setForced($this->getElement('force_check')->isChecked()) + ->setCheckTime($this->getElement('check_time')->getValue()->getTimestamp()); + $this->getTransport($request)->send($check); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + if (! $object->active_checks_enabled + && ! $this->Auth()->hasPermission('monitoring/command/schedule-check') + ) { + continue; + } + + $check = new ScheduleServiceCheckCommand(); + $check->setObject($object); + $this->scheduleCheck($check, $this->request); + } + Notification::success($this->translatePlural( + 'Scheduling service check..', + 'Scheduling service checks..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php new file mode 100644 index 0000000..90d50d4 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php @@ -0,0 +1,263 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use DateTime; +use DateInterval; +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand; +use Icinga\Web\Notification; +use Icinga\Web\Request; + +/** + * Form for scheduling service downtimes + */ +class ScheduleServiceDowntimeCommandForm extends ObjectsCommandForm +{ + /** + * Fixed downtime + */ + const FIXED = 'fixed'; + + /** + * Flexible downtime + */ + const FLEXIBLE = 'flexible'; + + /** @var DateTime downtime start */ + protected $start; + + /** @var DateTime fixed downtime end */ + protected $fixedEnd; + + /** @var DateTime flexible downtime end */ + protected $flexibleEnd; + + /** @var DateInterval flexible downtime duration */ + protected $flexibleDuration; + + /** @var mixed Comment text */ + protected $commentText; + + /** + * Initialize this form + */ + public function init() + { + $this->start = new DateTime(); + + $config = Config::module('monitoring'); + + $this->commentText = $config->get('settings', 'servicedowntime_comment_text'); + $fixedEnd = clone $this->start; + $fixed = $config->get('settings', 'servicedowntime_end_fixed', 'PT1H'); + $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed)); + + $flexibleEnd = clone $this->start; + $flexible = $config->get('settings', 'servicedowntime_end_flexible', 'PT1H'); + $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible)); + + $flexibleDuration = $config->get('settings', 'servicedowntime_flexible_duration', 'PT2H'); + $this->flexibleDuration = new DateInterval($flexibleDuration); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation. + */ + public function getSubmitLabel() + { + return $this->translatePlural('Schedule downtime', 'Schedule downtimes', count($this->objects)); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $this->addDescription($this->translate( + 'This command is used to schedule host and service downtimes. During the specified downtime,' + . ' Icinga will not send notifications out about the hosts and services. When the scheduled' + . ' downtime expires, Icinga will send out notifications for the hosts and services as it' + . ' normally would. Scheduled downtimes are preserved across program shutdowns and' + . ' restarts.' + )); + + $isFlexible = (bool) isset($formData['type']) && $formData['type'] === self::FLEXIBLE; + + $this->addElements(array( + array( + 'textarea', + 'comment', + array( + 'required' => true, + 'label' => $this->translate('Comment'), + 'description' => $this->translate( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ), + 'attribs' => array('class' => 'autofocus'), + 'value' => $this->commentText + ) + ), + array( + 'dateTimePicker', + 'start', + array( + 'required' => true, + 'label' => $this->translate('Start Time'), + 'description' => $this->translate('Set the start date and time for the downtime.'), + 'value' => $this->start + ) + ), + array( + 'dateTimePicker', + 'end', + array( + 'required' => true, + 'label' => $this->translate('End Time'), + 'description' => $this->translate('Set the end date and time for the downtime.'), + 'preserveDefault' => true, + 'value' => $isFlexible ? $this->flexibleEnd : $this->fixedEnd + ) + ), + array( + 'select', + 'type', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Type'), + 'description' => $this->translate( + 'If you select the fixed option, the downtime will be in effect between the start and end' + . ' times you specify whereas a flexible downtime starts when the host or service enters a' + . ' problem state sometime between the start and end times you specified and lasts as long' + . ' as the duration time you enter. The duration fields do not apply for fixed downtimes.' + ), + 'multiOptions' => array( + self::FIXED => $this->translate('Fixed'), + self::FLEXIBLE => $this->translate('Flexible') + ), + 'validators' => array( + array( + 'InArray', + true, + array(array(self::FIXED, self::FLEXIBLE)) + ) + ) + ) + ) + )); + $this->addDisplayGroup( + array('start', 'end'), + 'start-end', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div')) + ) + ) + ); + if ($isFlexible) { + $this->addElements(array( + array( + 'number', + 'hours', + array( + 'required' => true, + 'label' => $this->translate('Hours'), + 'value' => $this->flexibleDuration->h, + 'min' => -1 + ) + ), + array( + 'number', + 'minutes', + array( + 'required' => true, + 'label' => $this->translate('Minutes'), + 'value' => $this->flexibleDuration->m, + 'min' => -1 + ) + ) + )); + $this->addDisplayGroup( + array('hours', 'minutes'), + 'duration', + array( + 'legend' => $this->translate('Flexible Duration'), + 'description' => $this->translate( + 'Enter here the duration of the downtime. The downtime will be automatically deleted after this' + . ' time expired.' + ), + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div')), + array( + 'Description', + array('tag' => 'span', 'class' => 'description', 'placement' => 'prepend') + ), + 'Fieldset' + ) + ) + ); + } + return $this; + } + + public function scheduleDowntime(ScheduleServiceDowntimeCommand $downtime, Request $request) + { + $downtime + ->setComment($this->getElement('comment')->getValue()) + ->setAuthor($request->getUser()->getUsername()) + ->setStart($this->getElement('start')->getValue()->getTimestamp()) + ->setEnd($this->getElement('end')->getValue()->getTimestamp()); + if ($this->getElement('type')->getValue() === self::FLEXIBLE) { + $downtime->setFixed(false); + $downtime->setDuration( + (float) $this->getElement('hours')->getValue() * 3600 + + (float) $this->getElement('minutes')->getValue() * 60 + ); + } + $this->getTransport($request)->send($downtime); + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + $end = $this->getValue('end')->getTimestamp(); + if ($end <= $this->getValue('start')->getTimestamp()) { + $endElement = $this->_elements['end']; + $endElement->setValue($endElement->getValue()->format($endElement->getFormat())); + $endElement->addError($this->translate('The end time must be greater than the start time')); + return false; + } + + $now = new DateTime; + if ($end <= $now->getTimestamp()) { + $endElement = $this->_elements['end']; + $endElement->setValue($endElement->getValue()->format($endElement->getFormat())); + $endElement->addError($this->translate('A downtime must not be in the past')); + return false; + } + + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $downtime = new ScheduleServiceDowntimeCommand(); + $downtime->setObject($object); + $this->scheduleDowntime($downtime, $this->request); + } + Notification::success($this->translatePlural( + 'Scheduling service downtime..', + 'Scheduling service downtimes..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php b/modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php new file mode 100644 index 0000000..0d1c393 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php @@ -0,0 +1,110 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Command\Object\SendCustomNotificationCommand; +use Icinga\Web\Notification; + +/** + * Form to send custom notifications + */ +class SendCustomNotificationCommandForm extends ObjectsCommandForm +{ + /** + * Initialize this form + */ + public function init() + { + $this->addDescription( + $this->translate('This command is used to send custom notifications about hosts or services.') + ); + } + + /** + * {@inheritdoc} + */ + public function getSubmitLabel() + { + return $this->translatePlural('Send custom notification', 'Send custom notifications', count($this->objects)); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + $config = Config::module('monitoring'); + + $this->addElements(array( + array( + 'textarea', + 'comment', + array( + 'required' => true, + 'label' => $this->translate('Comment'), + 'description' => $this->translate( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ) + ) + ), + array( + 'checkbox', + 'forced', + array( + 'label' => $this->translate('Forced'), + 'value' => (bool) $config->get('settings', 'custom_notification_forced', false), + 'description' => $this->translate( + 'If you check this option, the notification is sent out regardless of time restrictions and' + . ' whether or not notifications are enabled.' + ) + ) + ) + )); + + if (! $this->getBackend()->isIcinga2()) { + $this->addElement( + 'checkbox', + 'broadcast', + array( + 'label' => $this->translate('Broadcast'), + 'value' => (bool) $config->get('settings', 'custom_notification_broadcast', false), + 'description' => $this->translate( + 'If you check this option, the notification is sent out to all normal and escalated contacts.' + ) + ) + ); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function onSuccess() + { + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + $notification = new SendCustomNotificationCommand(); + $notification + ->setObject($object) + ->setComment($this->getElement('comment')->getValue()) + ->setAuthor($this->request->getUser()->getUsername()) + ->setForced($this->getElement('forced')->isChecked()); + if (($broadcast = $this->getElement('broadcast')) !== null) { + $notification->setBroadcast($broadcast->isChecked()); + } + $this->getTransport($this->request)->send($notification); + } + Notification::success($this->translatePlural( + 'Sending custom notification..', + 'Sending custom notifications..', + count($this->objects) + )); + return true; + } +} diff --git a/modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php b/modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php new file mode 100644 index 0000000..e4aabb2 --- /dev/null +++ b/modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php @@ -0,0 +1,187 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Command\Object; + +use Icinga\Module\Monitoring\Command\Object\ToggleObjectFeatureCommand; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use Icinga\Web\Notification; + +/** + * Form for enabling or disabling features of Icinga objects, i.e. hosts or services + */ +class ToggleObjectFeaturesCommandForm extends ObjectsCommandForm +{ + /** + * Feature to feature spec map + * + * @var string[] + */ + protected $features; + + /** + * Feature to feature status map + * + * @var int[] + */ + protected $featureStatus; + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setUseFormAutosubmit(); + $this->setAttrib('class', self::DEFAULT_CLASSES . ' object-features'); + $features = array( + ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => array( + 'label' => $this->translate('Active Checks'), + 'permission' => 'monitoring/command/feature/object/active-checks' + ), + ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => array( + 'label' => $this->translate('Passive Checks'), + 'permission' => 'monitoring/command/feature/object/passive-checks' + ), + ToggleObjectFeatureCommand::FEATURE_OBSESSING => array( + 'label' => $this->translate('Obsessing'), + 'permission' => 'monitoring/command/feature/object/obsessing' + ), + ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => array( + 'label' => $this->translate('Notifications'), + 'permission' => 'monitoring/command/feature/object/notifications' + ), + ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => array( + 'label' => $this->translate('Event Handler'), + 'permission' => 'monitoring/command/feature/object/event-handler' + ), + ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => array( + 'label' => $this->translate('Flap Detection'), + 'permission' => 'monitoring/command/feature/object/flap-detection' + ) + ); + if ($this->getBackend()->isIcinga2()) { + unset($features[ToggleObjectFeatureCommand::FEATURE_OBSESSING]); + } + $this->features = $features; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + foreach ($this->features as $feature => $spec) { + $options = array( + 'autosubmit' => true, + 'disabled' => $this->hasPermission($spec['permission']) ? null : 'disabled', + 'label' => $spec['label'] + ); + if ($formData[$feature . '_changed']) { + $options['description'] = $this->translate('changed'); + } + if ($formData[$feature] === 2) { + $this->addElement('select', $feature, $options + [ + 'description' => $this->translate('Multiple Values'), + 'filters' => [['Null', ['type' => \Zend_Filter_Null::STRING]]], + 'multiOptions' => [ + '' => $this->translate('Leave Unchanged'), + $this->translate('Disable All'), + $this->translate('Enable All') + ], + 'decorators' => array_merge( + array_slice(static::$defaultElementDecorators, 0, 3), + [['Description', ['tag' => 'span']]], + array_slice(static::$defaultElementDecorators, 4, 1), + [['HtmlTag', ['tag' => 'div', 'class' => 'control-group indeterminate']]] + ) + ]); + } else { + $options['value'] = $formData[$feature]; + $this->addElement('checkbox', $feature, $options); + } + } + } + + /** + * Load feature status + * + * @param MonitoredObject|object $object + * + * @return $this + */ + public function load($object) + { + $featureStatus = array(); + foreach (array_keys($this->features) as $feature) { + $featureStatus[$feature] = $object->{$feature}; + if (isset($object->{$feature . '_changed'})) { + $featureStatus[$feature . '_changed'] = (bool) $object->{$feature . '_changed'}; + } else { + $featureStatus[$feature . '_changed'] = false; + } + } + $this->create($featureStatus); + $this->featureStatus = $featureStatus; + + return $this; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::onSuccess() For the method documentation. + */ + public function onSuccess() + { + $notifications = array( + ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => array( + $this->translate('Enabling active checks..'), + $this->translate('Disabling active checks..') + ), + ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => array( + $this->translate('Enabling passive checks..'), + $this->translate('Disabling passive checks..') + ), + ToggleObjectFeatureCommand::FEATURE_OBSESSING => array( + $this->translate('Enabling obsessing..'), + $this->translate('Disabling obsessing..') + ), + ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => array( + $this->translate('Enabling notifications..'), + $this->translate('Disabling notifications..') + ), + ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => array( + $this->translate('Enabling event handler..'), + $this->translate('Disabling event handler..') + ), + ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => array( + $this->translate('Enabling flap detection..'), + $this->translate('Disabling flap detection..') + ) + ); + + foreach ($this->getValues() as $feature => $enabled) { + if ($this->getElement($feature)->getAttrib('disabled') !== null + || $enabled === null + || (int) $enabled === (int) $this->featureStatus[$feature] + ) { + continue; + } + foreach ($this->objects as $object) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + if ((bool) $object->{$feature} !== (bool) $enabled) { + $toggleFeature = new ToggleObjectFeatureCommand(); + $toggleFeature + ->setFeature($feature) + ->setObject($object) + ->setEnabled($enabled); + $this->getTransport($this->request)->send($toggleFeature); + } + } + Notification::success( + $notifications[$feature][$enabled ? 0 : 1] + ); + } + + return true; + } +} diff --git a/modules/monitoring/application/forms/Config/BackendConfigForm.php b/modules/monitoring/application/forms/Config/BackendConfigForm.php new file mode 100644 index 0000000..5ed42e1 --- /dev/null +++ b/modules/monitoring/application/forms/Config/BackendConfigForm.php @@ -0,0 +1,367 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config; + +use Exception; +use InvalidArgumentException; +use Icinga\Application\Config; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\ConfigForm; +use Icinga\Web\Form; + +/** + * Form for managing monitoring backends + */ +class BackendConfigForm extends ConfigForm +{ + /** + * The available monitoring backend resources split by type + * + * @var array + */ + protected $resources; + + /** + * The backend to load when displaying the form for the first time + * + * @var string + */ + protected $backendToLoad; + + /** + * Initialize this form + */ + public function init() + { + $this->setName('form_config_monitoring_backends'); + $this->setSubmitLabel($this->translate('Save Changes')); + } + + /** + * Set the resource configuration to use + * + * @param Config $resourceConfig The resource configuration + * + * @return $this + * + * @throws ConfigurationError In case there are no valid monitoring backend resources + */ + public function setResourceConfig(Config $resourceConfig) + { + $resources = array(); + foreach ($resourceConfig as $name => $resource) { + if ($resource->type === 'db') { + $resources['ido'][$name] = $name; + } + } + + if (empty($resources)) { + throw new ConfigurationError($this->translate( + 'Could not find any valid monitoring backend resources. Please configure a database resource first.' + )); + } + + $this->resources = $resources; + return $this; + } + + /** + * Populate the form with the given backend's config + * + * @param string $name + * + * @return $this + * + * @throws NotFoundError In case no backend with the given name is found + */ + public function load($name) + { + if (! $this->config->hasSection($name)) { + throw new NotFoundError('No monitoring backend called "%s" found', $name); + } + + $this->backendToLoad = $name; + return $this; + } + + /** + * Add a new monitoring backend + * + * The backend to add is identified by the array-key `name'. + * + * @param array $data + * + * @return $this + * + * @throws InvalidArgumentException In case $data does not contain a backend name + * @throws IcingaException In case a backend with the same name already exists + */ + public function add(array $data) + { + if (! isset($data['name'])) { + throw new InvalidArgumentException('Key \'name\' missing'); + } + + $backendName = $data['name']; + if ($this->config->hasSection($backendName)) { + throw new IcingaException( + $this->translate('A monitoring backend with the name "%s" does already exist'), + $backendName + ); + } + + unset($data['name']); + $this->config->setSection($backendName, $data); + return $this; + } + + /** + * Edit a monitoring backend + * + * @param string $name + * @param array $data + * + * @return $this + * + * @throws NotFoundError In case no backend with the given name is found + */ + public function edit($name, array $data) + { + if (! $this->config->hasSection($name)) { + throw new NotFoundError('No monitoring backend called "%s" found', $name); + } + + $backendConfig = $this->config->getSection($name); + if (isset($data['name'])) { + if ($data['name'] !== $name) { + $this->config->removeSection($name); + $name = $data['name']; + } + + unset($data['name']); + } + + $backendConfig->merge($data); + $this->config->setSection($name, $backendConfig); + return $this; + } + + /** + * Remove a monitoring backend + * + * @param string $name + * + * @return $this + */ + public function delete($name) + { + $this->config->removeSection($name); + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $this->addElement( + 'checkbox', + 'disabled', + array( + 'label' => $this->translate('Disable This Backend') + ) + ); + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => $this->translate('Backend Name'), + 'description' => $this->translate( + 'The name of this monitoring backend that is used to differentiate it from others' + ) + ) + ); + + $resourceType = isset($formData['type']) ? $formData['type'] : null; + + $resourceTypes = array(); + if ($resourceType === 'ido' || array_key_exists('ido', $this->resources)) { + $resourceTypes['ido'] = 'IDO Backend'; + } + + if ($resourceType === null) { + $resourceType = key($resourceTypes); + } + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Backend Type'), + 'description' => $this->translate( + 'The type of data source used for retrieving monitoring information' + ), + 'multiOptions' => $resourceTypes + ) + ); + + $this->addElement( + 'select', + 'resource', + array( + 'required' => true, + 'label' => $this->translate('Resource'), + 'description' => $this->translate('The resource to use'), + 'multiOptions' => $this->resources[$resourceType], + 'value' => current($this->resources[$resourceType]), + 'autosubmit' => true + ) + ); + $resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource'); + $this->addElement( + 'note', + 'resource_note', + array( + 'escape' => false, + 'value' => sprintf( + '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>', + $this->getView()->url('config/editresource', array('resource' => $resourceName)), + sprintf($this->translate('Show the configuration of the %s resource'), $resourceName), + $this->translate('Show resource configuration') + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + // In case another error occured and the checkbox was displayed before + $this->addSkipValidationCheckbox(); + } + } + + /** + * Populate the configuration of the backend to load + */ + public function onRequest() + { + if ($this->backendToLoad) { + $data = $this->config->getSection($this->backendToLoad)->toArray(); + $data['name'] = $this->backendToLoad; + $this->populate($data); + } + } + + /** + * Return whether the given values are valid + * + * @param array $formData The data to validate + * + * @return bool + */ + public function isValid($formData) + { + if (! parent::isValid($formData)) { + return false; + } + + if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) { + $resourceConfig = ResourceFactory::getResourceConfig($this->getValue('resource')); + if (! self::isValidIdoSchema($this, $resourceConfig) + || (! $this->getElement('disabled')->isChecked() + && ! self::isValidIdoInstance($this, $resourceConfig)) + ) { + if ($el === null) { + $this->addSkipValidationCheckbox(); + } + + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the schema validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'order' => 0, + 'ignore' => true, + 'label' => $this->translate('Skip Validation'), + 'description' => $this->translate( + 'Check this to not to validate the IDO schema of the chosen resource.' + ) + ) + ); + } + + /** + * Return whether the given resource contains a valid IDO schema + * + * @param Form $form + * @param ConfigObject $resourceConfig + * + * @return bool + */ + public static function isValidIdoSchema(Form $form, ConfigObject $resourceConfig) + { + try { + $db = ResourceFactory::createResource($resourceConfig); + $db->select()->from('icinga_dbversion', array('version'))->fetchOne(); + } catch (Exception $_) { + $form->error($form->translate( + 'Cannot find the IDO schema. Please verify that the given database ' + . 'contains the schema and that the configured user has access to it.' + )); + return false; + } + + return true; + } + + /** + * Return whether a single icinga instance is writing to the given resource + * + * @param Form $form + * @param ConfigObject $resourceConfig + * + * @return bool True if it's a single instance, false if none + * or multiple instances are writing to it + */ + public static function isValidIdoInstance(Form $form, ConfigObject $resourceConfig) + { + $db = ResourceFactory::createResource($resourceConfig); + $rowCount = $db->select()->from('icinga_instances')->count(); + + if ($rowCount === 0) { + $form->warning($form->translate( + 'There is currently no icinga instance writing to the IDO. Make sure ' + . 'that a icinga instance is configured and able to write to the IDO.' + )); + return false; + } elseif ($rowCount > 1) { + $form->warning($form->translate( + 'There is currently more than one icinga instance writing to the IDO. You\'ll see all objects from all' + . ' instances without any differentation. If this is not desired, consider setting up a separate IDO' + . ' for each instance.' + )); + return false; + } + + return true; + } +} diff --git a/modules/monitoring/application/forms/Config/SecurityConfigForm.php b/modules/monitoring/application/forms/Config/SecurityConfigForm.php new file mode 100644 index 0000000..d57f985 --- /dev/null +++ b/modules/monitoring/application/forms/Config/SecurityConfigForm.php @@ -0,0 +1,75 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config; + +use Icinga\Web\Notification; +use Icinga\Forms\ConfigForm; + +/** + * Form for modifying security relevant settings + */ +class SecurityConfigForm extends ConfigForm +{ + /** + * Initialize this form + */ + public function init() + { + $this->setName('form_config_monitoring_security'); + $this->setSubmitLabel($this->translate('Save Changes')); + } + + /** + * @see Form::onSuccess() + */ + public function onSuccess() + { + $this->config->setSection('security', $this->getValues()); + + if ($this->save()) { + Notification::success($this->translate('New security configuration has successfully been stored')); + } else { + return false; + } + } + + /** + * @see Form::onRequest() + */ + public function onRequest() + { + $this->populate($this->config->getSection('security')->toArray()); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'protected_customvars', + array( + 'allowEmpty' => true, + 'attribs' => array('placeholder' => $this->getDefaultProtectedCustomvars()), + 'label' => $this->translate('Protected Custom Variables'), + 'description' => $this->translate( + 'Comma separated case insensitive list of protected custom variables.' + . ' Use * as a placeholder for zero or more wildcard characters.' + . ' Existence of those custom variables will be shown, but their values will be masked.' + ) + ) + ); + } + + /** + * Return the customvars to suggest to protect when none are protected + * + * @return string + */ + public function getDefaultProtectedCustomvars() + { + return '*pw*,*pass*,community'; + } +} diff --git a/modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php b/modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php new file mode 100644 index 0000000..3d501e0 --- /dev/null +++ b/modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php @@ -0,0 +1,75 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config\Transport; + +use Icinga\Web\Form; + +class ApiTransportForm extends Form +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setName('form_config_command_transport_api'); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData = array()) + { + $this->addElements(array( + array( + 'text', + 'host', + array( + 'required' => true, + 'label' => $this->translate('Host'), + 'description' => $this->translate( + 'Hostname or address of the remote Icinga instance' + ) + ) + ), + array( + 'number', + 'port', + array( + 'required' => true, + 'preserveDefault' => true, + 'label' => $this->translate('Port'), + 'description' => $this->translate('SSH port to connect to on the remote Icinga instance'), + 'value' => 5665 + ) + ), + array( + 'text', + 'username', + array( + 'required' => true, + 'label' => $this->translate('API Username'), + 'description' => $this->translate( + 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be' + . ' possible for this user' + ) + ) + ), + array( + 'password', + 'password', + array( + 'required' => true, + 'label' => $this->translate('API Password'), + 'description' => $this->translate( + 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be' + . ' possible for this user' + ), + 'renderPassword' => true + ) + ) + )); + + return $this; + } +} diff --git a/modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php b/modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php new file mode 100644 index 0000000..15c7357 --- /dev/null +++ b/modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php @@ -0,0 +1,37 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config\Transport; + +use Icinga\Web\Form; + +class LocalTransportForm extends Form +{ + /** + * (non-PHPDoc) + * @see Form::init() For the method documentation. + */ + public function init() + { + $this->setName('form_config_command_transport_local'); + } + + /** + * (non-PHPDoc) + * @see Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $this->addElement( + 'text', + 'path', + array( + 'required' => true, + 'label' => $this->translate('Command File'), + 'value' => '/var/run/icinga2/cmd/icinga2.cmd', + 'description' => $this->translate('Path to the local Icinga command file') + ) + ); + return $this; + } +} diff --git a/modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php b/modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php new file mode 100644 index 0000000..7beeacf --- /dev/null +++ b/modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php @@ -0,0 +1,185 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config\Transport; + +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\Web\Form; + +class RemoteTransportForm extends Form +{ + /** + * The available resources split by type + * + * @var array + */ + protected $resources; + + /** + * (non-PHPDoc) + * @see Form::init() For the method documentation. + */ + public function init() + { + $this->setName('form_config_command_transport_remote'); + } + + /** + * Load all available ssh identity resources + * + * @return $this + * + * @throws \Icinga\Exception\ConfigurationError + */ + public function loadResources() + { + $resourceConfig = ResourceFactory::getResourceConfigs(); + + $resources = array(); + foreach ($resourceConfig as $name => $resource) { + if ($resource->type === 'ssh') { + $resources['ssh'][$name] = $name; + } + } + + if (empty($resources)) { + throw new ConfigurationError($this->translate('Could not find any valid SSH resources')); + } + + $this->resources = $resources; + + return $this; + } + + /** + * Check whether ssh identity resources exists or not + * + * @return boolean + */ + public function hasResources() + { + $resourceConfig = ResourceFactory::getResourceConfigs(); + + foreach ($resourceConfig as $name => $resource) { + if ($resource->type === 'ssh') { + return true; + } + } + return false; + } + + /** + * (non-PHPDoc) + * @see Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $useResource = false; + + if ($this->hasResources()) { + $useResource = isset($formData['use_resource']) + ? $formData['use_resource'] : $this->getValue('use_resource'); + + $this->addElement( + 'checkbox', + 'use_resource', + array( + 'label' => $this->translate('Use SSH Identity'), + 'description' => $this->translate('Make use of the ssh identity resource'), + 'autosubmit' => true, + 'ignore' => true + ) + ); + } + + if ($useResource) { + $this->loadResources(); + + $decorators = static::$defaultElementDecorators; + array_pop($decorators); // Removes the HtmlTag decorator + + $this->addElement( + 'select', + 'resource', + array( + 'required' => true, + 'label' => $this->translate('SSH Identity'), + 'description' => $this->translate('The resource to use'), + 'decorators' => $decorators, + 'multiOptions' => $this->resources['ssh'], + 'value' => current($this->resources['ssh']), + 'autosubmit' => false + ) + ); + $resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource'); + $this->addElement( + 'note', + 'resource_note', + array( + 'escape' => false, + 'decorators' => $decorators, + 'value' => sprintf( + '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>', + $this->getView()->url('config/editresource', array('resource' => $resourceName)), + sprintf($this->translate('Show the configuration of the %s resource'), $resourceName), + $this->translate('Show resource configuration') + ) + ) + ); + } + + $this->addElements(array( + array( + 'text', + 'host', + array( + 'required' => true, + 'label' => $this->translate('Host'), + 'description' => $this->translate( + 'Hostname or address of the remote Icinga instance' + ) + ) + ), + array( + 'number', + 'port', + array( + 'required' => true, + 'preserveDefault' => true, + 'label' => $this->translate('Port'), + 'description' => $this->translate('SSH port to connect to on the remote Icinga instance'), + 'value' => 22 + ) + ) + )); + + if (! $useResource) { + $this->addElement( + 'text', + 'user', + array( + 'required' => true, + 'label' => $this->translate('User'), + 'description' => $this->translate( + 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be' + . ' possible for this user' + ) + ) + ); + } + + $this->addElement( + 'text', + 'path', + array( + 'required' => true, + 'label' => $this->translate('Command File'), + 'value' => '/var/run/icinga2/cmd/icinga2.cmd', + 'description' => $this->translate('Path to the Icinga command file on the remote Icinga instance') + ) + ); + + return $this; + } +} diff --git a/modules/monitoring/application/forms/Config/TransportConfigForm.php b/modules/monitoring/application/forms/Config/TransportConfigForm.php new file mode 100644 index 0000000..c68e63d --- /dev/null +++ b/modules/monitoring/application/forms/Config/TransportConfigForm.php @@ -0,0 +1,392 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config; + +use Icinga\Data\ConfigObject; +use Icinga\Module\Monitoring\Command\Transport\CommandTransport; +use Icinga\Module\Monitoring\Exception\CommandTransportException; +use InvalidArgumentException; +use Icinga\Application\Platform; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\ConfigForm; +use Icinga\Module\Monitoring\Command\Transport\ApiCommandTransport; +use Icinga\Module\Monitoring\Command\Transport\LocalCommandFile; +use Icinga\Module\Monitoring\Command\Transport\RemoteCommandFile; +use Icinga\Module\Monitoring\Forms\Config\Transport\ApiTransportForm; +use Icinga\Module\Monitoring\Forms\Config\Transport\LocalTransportForm; +use Icinga\Module\Monitoring\Forms\Config\Transport\RemoteTransportForm; + +/** + * Form for managing command transports + */ +class TransportConfigForm extends ConfigForm +{ + /** + * The transport to load when displaying the form for the first time + * + * @var string + */ + protected $transportToLoad; + + /** + * The names of all available Icinga instances + * + * @var array + */ + protected $instanceNames; + + /** + * @var bool + */ + protected $validatePartial = true; + + /** + * Initialize this form + */ + public function init() + { + $this->setName('form_config_command_transports'); + $this->setSubmitLabel($this->translate('Save Changes')); + } + + /** + * Set the names of all available Icinga instances + * + * @param array $names + * + * @return $this + */ + public function setInstanceNames(array $names) + { + $this->instanceNames = $names; + return $this; + } + + /** + * Return the names of all available Icinga instances + * + * @return array + */ + public function getInstanceNames() + { + return $this->instanceNames ?: array(); + } + + /** + * Return a form object for the given transport type + * + * @param string $type The transport type for which to return a form + * + * @return \Icinga\Web\Form + * + * @throws InvalidArgumentException In case the given transport type is invalid + */ + public function getTransportForm($type) + { + switch (strtolower($type)) { + case LocalCommandFile::TRANSPORT: + return new LocalTransportForm(); + case RemoteCommandFile::TRANSPORT: + return new RemoteTransportForm(); + case ApiCommandTransport::TRANSPORT: + return new ApiTransportForm(); + default: + throw new InvalidArgumentException( + sprintf($this->translate('Invalid command transport type "%s" given'), $type) + ); + } + } + + /** + * Populate the form with the given transport's config + * + * @param string $name + * + * @return $this + * + * @throws NotFoundError In case no transport with the given name is found + */ + public function load($name) + { + if (! $this->config->hasSection($name)) { + throw new NotFoundError('No command transport called "%s" found', $name); + } + + $this->transportToLoad = $name; + return $this; + } + + /** + * Add a new command transport + * + * The transport to add is identified by the array-key `name'. + * + * @param array $data + * + * @return $this + * + * @throws InvalidArgumentException In case $data does not contain a transport name + * @throws IcingaException In case a transport with the same name already exists + */ + public function add(array $data) + { + if (! isset($data['name'])) { + throw new InvalidArgumentException('Key \'name\' missing'); + } + + $transportName = $data['name']; + if ($this->config->hasSection($transportName)) { + throw new IcingaException( + $this->translate('A command transport with the name "%s" does already exist'), + $transportName + ); + } + + unset($data['name']); + $this->config->setSection($transportName, $data); + return $this; + } + + /** + * Edit an existing command transport + * + * @param string $name + * @param array $data + * + * @return $this + * + * @throws NotFoundError In case no transport with the given name is found + */ + public function edit($name, array $data) + { + if (! $this->config->hasSection($name)) { + throw new NotFoundError('No command transport called "%s" found', $name); + } + + $transportConfig = $this->config->getSection($name); + if (isset($data['name'])) { + if ($data['name'] !== $name) { + $this->config->removeSection($name); + $name = $data['name']; + } + + unset($data['name']); + } + + $transportConfig->merge($data); + $this->config->setSection($name, $transportConfig); + return $this; + } + + /** + * Remove a command transport + * + * @param string $name + * + * @return $this + */ + public function delete($name) + { + $this->config->removeSection($name); + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $instanceNames = $this->getInstanceNames(); + if (count($instanceNames) > 1) { + $options = array('none' => $this->translate('None', 'command transport instance association')); + $this->addElement( + 'select', + 'instance', + array( + 'label' => $this->translate('Instance Link'), + 'description' => $this->translate( + 'The name of the Icinga instance this transport should exclusively transfer commands to.' + ), + 'multiOptions' => array_merge($options, array_combine($instanceNames, $instanceNames)) + ) + ); + } + + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => $this->translate('Transport Name'), + 'description' => $this->translate( + 'The name of this command transport that is used to differentiate it from others' + ) + ) + ); + + $transportTypes = array( + ApiCommandTransport::TRANSPORT => $this->translate('Icinga 2 API'), + LocalCommandFile::TRANSPORT => $this->translate('Local Command File'), + RemoteCommandFile::TRANSPORT => $this->translate('Remote Command File') + ); + if (! Platform::extensionLoaded('curl')) { + unset($transportTypes[ApiCommandTransport::TRANSPORT]); + } + + $transportType = isset($formData['transport']) ? $formData['transport'] : null; + if ($transportType === null) { + $transportType = key($transportTypes); + } + + $this->addElements(array( + array( + 'select', + 'transport', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Transport Type'), + 'multiOptions' => $transportTypes + ) + ) + )); + + $this->addSubForm($this->getTransportForm($transportType)->create($formData), 'transport_form'); + } + + /** + * Add a submit button to this form and one to manually validate the configuration + * + * Calls parent::addSubmitButton() to add the submit button. + * + * @return $this + */ + public function addSubmitButton() + { + parent::addSubmitButton(); + + if ($this->getSubForm('transport_form') instanceof ApiTransportForm) { + $btnSubmit = $this->getElement('btn_submit'); + + if ($btnSubmit !== null) { + // In the setup wizard $this is being used as a subform which doesn't have a submit button. + $this->addElement( + 'submit', + 'transport_validation', + array( + 'ignore' => true, + 'label' => $this->translate('Validate Configuration'), + 'data-progress-label' => $this->translate('Validation In Progress'), + 'decorators' => array('ViewHelper') + ) + ); + + $this->setAttrib('data-progress-element', 'transport-progress'); + $this->addElement( + 'note', + 'transport-progress', + array( + 'decorators' => array( + 'ViewHelper', + array('Spinner', array('id' => 'transport-progress')) + ) + ) + ); + + $elements = array('transport_validation', 'transport-progress'); + + $btnSubmit->setDecorators(array('ViewHelper')); + array_unshift($elements, 'btn_submit'); + + $this->addDisplayGroup( + $elements, + 'submit_validation', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ) + ) + ); + } + } + + return $this; + } + + /** + * Populate the configuration of the transport to load + */ + public function onRequest() + { + if ($this->transportToLoad) { + $data = $this->config->getSection($this->transportToLoad)->toArray(); + $data['name'] = $this->transportToLoad; + $this->populate($data); + } + } + + /** + * {@inheritdoc} + */ + public function isValidPartial(array $formData) + { + $isValidPartial = parent::isValidPartial($formData); + + $transportValidation = $this->getElement('transport_validation'); + if ($transportValidation !== null && $transportValidation->isChecked() && $this->isValid($formData)) { + $this->info($this->translate('The configuration has been successfully validated.')); + } + + return $isValidPartial; + } + + /** + * {@inheritdoc} + */ + public function isValid($formData) + { + if (! parent::isValid($formData)) { + return false; + } + + if ($this->getSubForm('transport_form') instanceof ApiTransportForm) { + if (! isset($formData['transport_validation']) + && isset($formData['force_creation']) && $formData['force_creation'] + ) { + // ignore any validation result + return true; + } + + try { + CommandTransport::createTransport(new ConfigObject($this->getValues()))->probe(); + } catch (CommandTransportException $e) { + $this->error(sprintf( + $this->translate('Failed to successfully validate the configuration: %s'), + $e->getMessage() + )); + + $this->addElement( + 'checkbox', + 'force_creation', + array( + 'order' => 0, + 'ignore' => true, + 'label' => $this->translate('Force Changes'), + 'description' => $this->translate( + 'Check this box to enforce changes without connectivity validation' + ) + ) + ); + + return false; + } + } + + return true; + } +} diff --git a/modules/monitoring/application/forms/Config/TransportReorderForm.php b/modules/monitoring/application/forms/Config/TransportReorderForm.php new file mode 100644 index 0000000..f3efe4c --- /dev/null +++ b/modules/monitoring/application/forms/Config/TransportReorderForm.php @@ -0,0 +1,87 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Config; + +use Icinga\Application\Config; +use Icinga\Web\Form; +use Icinga\Web\Notification; + +/** + * Form for reordering command transports + */ +class TransportReorderForm extends Form +{ + /** + * Initialize this form + */ + public function init() + { + $this->setName('form_reorder_command_transports'); + $this->setViewScript('form/reorder-command-transports.phtml'); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess() + $this->addElement( + 'hidden', + 'transport_newpos', + array( + 'required' => true, + 'validators' => array( + array( + 'validator' => 'regex', + 'options' => array( + 'pattern' => '/\A\d+\|/' + ) + ) + ) + ) + ); + } + + /** + * Update the command transport order and save the configuration + */ + public function onSuccess() + { + list($position, $transportName) = explode('|', $this->getValue('transport_newpos'), 2); + $config = $this->getConfig(); + if (! $config->hasSection($transportName)) { + Notification::error(sprintf($this->translate('Command transport "%s" not found'), $transportName)); + return false; + } + + if ($config->count() > 1) { + $sections = $config->keys(); + array_splice($sections, array_search($transportName, $sections, true), 1); + array_splice($sections, $position, 0, array($transportName)); + + $sectionsInNewOrder = array(); + foreach ($sections as $section) { + $sectionsInNewOrder[$section] = $config->getSection($section); + $config->removeSection($section); + } + foreach ($sectionsInNewOrder as $name => $options) { + $config->setSection($name, $options); + } + + $config->saveIni(); + Notification::success($this->translate('Command transport order updated')); + } + } + + /** + * Get the command transports config + * + * @return Config + */ + public function getConfig() + { + return Config::module('monitoring', 'commandtransports'); + } +} diff --git a/modules/monitoring/application/forms/EventOverviewForm.php b/modules/monitoring/application/forms/EventOverviewForm.php new file mode 100644 index 0000000..db1511c --- /dev/null +++ b/modules/monitoring/application/forms/EventOverviewForm.php @@ -0,0 +1,157 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms; + +use Icinga\Web\Url; +use Icinga\Web\Form; +use Icinga\Data\Filter\Filter; + +/** + * Configure the filter for the event overview + */ +class EventOverviewForm extends Form +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setName('form_event_overview'); + $this->setDecorators(array( + 'FormElements', + array('HtmlTag', array('tag' => 'div', 'class' => 'hbox')), + 'Form' + )); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $decorators = array( + array('Label', array('class' => 'optional')), + 'ViewHelper', + array('HtmlTag', array('tag' => 'div', 'class' => 'hbox-item optionbox')), + ); + + $url = Url::fromRequest()->getAbsoluteUrl(); + $this->addElement( + 'checkbox', + 'statechange', + array( + 'label' => $this->translate('State Changes'), + 'class' => 'autosubmit', + 'decorators' => $decorators, + 'value' => strpos($url, $this->stateChangeFilter()->toQueryString()) === false ? 0 : 1 + ) + ); + $this->addElement( + 'checkbox', + 'downtime', + array( + 'label' => $this->translate('Downtimes'), + 'class' => 'autosubmit', + 'decorators' => $decorators, + 'value' => strpos($url, $this->downtimeFilter()->toQueryString()) === false ? 0 : 1 + ) + ); + $this->addElement( + 'checkbox', + 'comment', + array( + 'label' => $this->translate('Comments'), + 'class' => 'autosubmit', + 'decorators' => $decorators, + 'value' => strpos($url, $this->commentFilter()->toQueryString()) === false ? 0 : 1 + ) + ); + $this->addElement( + 'checkbox', + 'notification', + array( + 'label' => $this->translate('Notifications'), + 'class' => 'autosubmit', + 'decorators' => $decorators, + 'value' => strpos($url, $this->notificationFilter()->toQueryString()) === false ? 0 : 1 + ) + ); + $this->addElement( + 'checkbox', + 'flapping', + array( + 'label' => $this->translate('Flapping'), + 'class' => 'autosubmit', + 'decorators' => $decorators, + 'value' => strpos($url, $this->flappingFilter()->toQueryString()) === false ? 0 : 1 + ) + ); + } + + /** + * Return the corresponding filter-object + * + * @returns Filter + */ + public function getFilter() + { + $filters = array(); + if ($this->getValue('statechange', 1)) { + $filters[] = $this->stateChangeFilter(); + } + if ($this->getValue('comment', 1)) { + $filters[] = $this->commentFilter(); + } + if ($this->getValue('notification', 1)) { + $filters[] = $this->notificationFilter(); + } + if ($this->getValue('downtime', 1)) { + $filters[] = $this->downtimeFilter(); + } + if ($this->getValue('flapping', 1)) { + $filters[] = $this->flappingFilter(); + } + return Filter::matchAny($filters); + } + + public function stateChangeFilter() + { + return Filter::matchAny( + Filter::expression('type', '=', 'hard_state'), + Filter::expression('type', '=', 'soft_state') + ); + } + + public function commentFilter() + { + return Filter::matchAny( + Filter::expression('type', '=', 'comment'), + Filter::expression('type', '=', 'comment_deleted'), + Filter::expression('type', '=', 'dt_comment'), + Filter::expression('type', '=', 'dt_comment_deleted'), + Filter::expression('type', '=', 'ack') + ); + } + + public function notificationFilter() + { + return Filter::expression('type', '=', 'notify'); + } + + public function downtimeFilter() + { + return Filter::matchAny( + Filter::expression('type', '=', 'downtime_start'), + Filter::expression('type', '=', 'downtime_end') + ); + } + + public function flappingFilter() + { + return Filter::matchAny( + Filter::expression('type', '=', 'flapping'), + Filter::expression('type', '=', 'flapping_deleted') + ); + } +} diff --git a/modules/monitoring/application/forms/Navigation/ActionForm.php b/modules/monitoring/application/forms/Navigation/ActionForm.php new file mode 100644 index 0000000..81d5588 --- /dev/null +++ b/modules/monitoring/application/forms/Navigation/ActionForm.php @@ -0,0 +1,79 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Navigation; + +use Icinga\Data\Filter\Filter; +use Icinga\Exception\QueryException; +use Icinga\Forms\Navigation\NavigationItemForm; + +class ActionForm extends NavigationItemForm +{ + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + parent::createElements($formData); + + $this->addElement( + 'text', + 'filter', + array( + 'allowEmpty' => true, + 'label' => $this->translate('Filter'), + 'description' => $this->translate( + 'Display this action only for objects matching this filter. Leave it blank' + . ' if you want this action being displayed regardless of the object' + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function isValid($formData) + { + if (! parent::isValid($formData)) { + return false; + } + + if (($filterString = $this->getValue('filter')) !== null) { + $filter = Filter::matchAll(); + $filter->setAllowedFilterColumns(array( + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + 'contact_name', + 'contactgroup_name', + function ($c) { + return preg_match('/^_(?:host|service)_/', $c); + } + )); + + try { + $filter->addFilter(Filter::fromQueryString($filterString)); + } catch (QueryException $_) { + $this->getElement('filter')->addError(sprintf( + $this->translate('Invalid filter provided. You can only use the following columns: %s'), + implode(', ', array( + 'instance_name', + 'host_name', + 'hostgroup_name', + 'service_description', + 'servicegroup_name', + 'contact_name', + 'contactgroup_name', + '_(host|service)_<customvar-name>' + )) + )); + return false; + } + } + + return true; + } +} diff --git a/modules/monitoring/application/forms/Navigation/HostActionForm.php b/modules/monitoring/application/forms/Navigation/HostActionForm.php new file mode 100644 index 0000000..da237d4 --- /dev/null +++ b/modules/monitoring/application/forms/Navigation/HostActionForm.php @@ -0,0 +1,8 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Navigation; + +class HostActionForm extends ActionForm +{ +} diff --git a/modules/monitoring/application/forms/Navigation/ServiceActionForm.php b/modules/monitoring/application/forms/Navigation/ServiceActionForm.php new file mode 100644 index 0000000..68314d1 --- /dev/null +++ b/modules/monitoring/application/forms/Navigation/ServiceActionForm.php @@ -0,0 +1,8 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Navigation; + +class ServiceActionForm extends ActionForm +{ +} diff --git a/modules/monitoring/application/forms/Setup/BackendPage.php b/modules/monitoring/application/forms/Setup/BackendPage.php new file mode 100644 index 0000000..d5c7efb --- /dev/null +++ b/modules/monitoring/application/forms/Setup/BackendPage.php @@ -0,0 +1,51 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Setup; + +use Icinga\Web\Form; +use Icinga\Application\Platform; + +class BackendPage extends Form +{ + public function init() + { + $this->setName('setup_monitoring_backend'); + $this->setTitle($this->translate('Monitoring Backend', 'setup.page.title')); + $this->addDescription($this->translate( + 'Please configure below how Icinga Web 2 should retrieve monitoring information.' + )); + } + + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'value' => 'icinga', + 'label' => $this->translate('Backend Name'), + 'description' => $this->translate('The identifier of this backend') + ) + ); + + $resourceTypes = array(); + if (Platform::hasMysqlSupport() || Platform::hasPostgresqlSupport()) { + $resourceTypes['ido'] = 'IDO'; + } + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'label' => $this->translate('Backend Type'), + 'description' => $this->translate( + 'The data source used for retrieving monitoring information' + ), + 'multiOptions' => $resourceTypes + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/IdoResourcePage.php b/modules/monitoring/application/forms/Setup/IdoResourcePage.php new file mode 100644 index 0000000..d648579 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/IdoResourcePage.php @@ -0,0 +1,188 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Setup; + +use Icinga\Data\ConfigObject; +use Icinga\Forms\Config\ResourceConfigForm; +use Icinga\Forms\Config\Resource\DbResourceForm; +use Icinga\Web\Form; +use Icinga\Module\Monitoring\Forms\Config\BackendConfigForm; +use Icinga\Module\Setup\Utils\DbTool; + +class IdoResourcePage extends Form +{ + /** + * Initialize this form + */ + public function init() + { + $this->setName('setup_monitoring_ido'); + $this->setTitle($this->translate('Monitoring IDO Resource', 'setup.page.title')); + $this->addDescription($this->translate( + 'Please fill out the connection details below to access the IDO database of your monitoring environment.' + )); + $this->setValidatePartial(true); + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'db' + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + // In case another error occured and the checkbox was displayed before + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $dbResourceForm = new DbResourceForm(); + $this->addElements($dbResourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icinga_ido'); + } + + /** + * Return whether the given values are valid + * + * @param array $formData The data to validate + * + * @return bool + */ + public function isValid($formData) + { + if (! parent::isValid($formData)) { + return false; + } + + if (! isset($formData['skip_validation']) || !$formData['skip_validation']) { + if (! $this->validateConfiguration()) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Run the configured backend's inspection checks and show the result, if necessary + * + * This will only run any validation if the user pushed the 'backend_validation' button. + * + * @param array $formData + * + * @return bool + */ + public function isValidPartial(array $formData) + { + if (isset($formData['backend_validation']) && parent::isValid($formData)) { + if (! $this->validateConfiguration(true)) { + return false; + } + + $this->info($this->translate('The configuration has been successfully validated.')); + } elseif (! isset($formData['backend_validation'])) { + // This is usually done by isValid(Partial), but as we're not calling any of these... + $this->populate($formData); + } + + return true; + } + + /** + * Return whether the configuration is valid + * + * @param bool $showLog Whether to show the validation log + * + * @return bool + */ + protected function validateConfiguration($showLog = false) + { + $inspection = ResourceConfigForm::inspectResource($this); + if ($inspection !== null) { + if ($showLog) { + $join = function ($e) use (&$join) { + return is_string($e) ? $e : join("\n", array_map($join, $e)); + }; + $this->addElement( + 'note', + 'inspection_output', + array( + 'order' => 0, + 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n" + . join("\n", array_map($join, $inspection->toArray())), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')), + ) + ) + ); + } + + if ($inspection->hasError()) { + $this->error(sprintf( + $this->translate('Failed to successfully validate the configuration: %s'), + $inspection->getError() + )); + return false; + } + } + + $configObject = new ConfigObject($this->getValues()); + if (! BackendConfigForm::isValidIdoSchema($this, $configObject) + || !BackendConfigForm::isValidIdoInstance($this, $configObject) + ) { + return false; + } + + if ($this->getValue('db') === 'pgsql') { + $db = new DbTool($this->getValues()); + $version = $db->connectToDb()->getServerVersion(); + if (version_compare($version, '9.1', '<')) { + $this->error(sprintf( + $this->translate('The server\'s version %s is too old. The minimum required version is %s.'), + $version, + '9.1' + )); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the configuration validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => $this->translate('Skip Validation'), + 'description' => $this->translate('Check this to not to validate the configuration') + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/SecurityPage.php b/modules/monitoring/application/forms/Setup/SecurityPage.php new file mode 100644 index 0000000..999103c --- /dev/null +++ b/modules/monitoring/application/forms/Setup/SecurityPage.php @@ -0,0 +1,27 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Setup; + +use Icinga\Web\Form; +use Icinga\Module\Monitoring\Forms\Config\SecurityConfigForm; + +class SecurityPage extends Form +{ + public function init() + { + $this->setName('setup_monitoring_security'); + $this->setTitle($this->translate('Monitoring Security', 'setup.page.title')); + $this->addDescription($this->translate( + 'To protect your monitoring environment against prying eyes please fill out the settings below.' + )); + } + + public function createElements(array $formData) + { + $securityConfigForm = new SecurityConfigForm(); + $securityConfigForm->createElements($formData); + $this->addElements($securityConfigForm->getElements()); + $this->getElement('protected_customvars')->setValue($securityConfigForm->getDefaultProtectedCustomvars()); + } +} diff --git a/modules/monitoring/application/forms/Setup/TransportPage.php b/modules/monitoring/application/forms/Setup/TransportPage.php new file mode 100644 index 0000000..9d0760a --- /dev/null +++ b/modules/monitoring/application/forms/Setup/TransportPage.php @@ -0,0 +1,55 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Setup; + +use Icinga\Web\Form; +use Icinga\Module\Monitoring\Forms\Config\TransportConfigForm; + +class TransportPage extends Form +{ + public function init() + { + $this->setName('setup_command_transport'); + $this->setTitle($this->translate('Command Transport', 'setup.page.title')); + $this->addDescription($this->translate( + 'Please define below how you want to send commands to your monitoring instance.' + )); + $this->setValidatePartial(true); + } + + public function createElements(array $formData) + { + $transportConfigForm = new TransportConfigForm(); + $this->addSubForm($transportConfigForm, 'transport_form'); + $transportConfigForm->create($formData); + $transportConfigForm->removeElement('instance'); + $transportConfigForm->getElement('name')->setValue('icinga2'); + } + + public function getValues($suppressArrayNotation = false) + { + return $this->getSubForm('transport_form')->getValues($suppressArrayNotation); + } + + /** + * Run the configured backend's inspection checks and show the result, if necessary + * + * This will only run any validation if the user pushed the 'transport_validation' button. + * + * @param array $formData + * + * @return bool + */ + public function isValidPartial(array $formData) + { + if (isset($formData['transport_validation']) && parent::isValid($formData)) { + $this->info($this->translate('The configuration has been successfully validated.')); + } elseif (! isset($formData['transport_validation'])) { + // This is usually done by isValid(Partial), but as we're not calling any of these... + $this->populate($formData); + } + + return true; + } +} diff --git a/modules/monitoring/application/forms/Setup/WelcomePage.php b/modules/monitoring/application/forms/Setup/WelcomePage.php new file mode 100644 index 0000000..aa78db5 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/WelcomePage.php @@ -0,0 +1,63 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms\Setup; + +use Icinga\Web\Form; + +class WelcomePage extends Form +{ + public function init() + { + $this->setName('setup_monitoring_welcome'); + } + + public function createElements(array $formData) + { + $this->addElement( + 'note', + 'welcome', + array( + 'value' => $this->translate( + 'Welcome to the configuration of the monitoring module for Icinga Web 2!' + ), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ); + + $this->addElement( + 'note', + 'core_hint', + array( + 'value' => $this->translate('This is the core module for Icinga Web 2.'), + 'decorators' => array('ViewHelper') + ) + ); + + $this->addElement( + 'note', + 'description', + array( + 'value' => $this->translate( + 'It offers various status and reporting views with powerful filter capabilities that allow' + . ' you to keep track of the most important events in your monitoring environment.' + ), + 'decorators' => array('ViewHelper') + ) + ); + + $this->addDisplayGroup( + array('core_hint', 'description'), + 'info', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div', 'class' => 'info')) + ) + ) + ); + } +} diff --git a/modules/monitoring/application/forms/StatehistoryForm.php b/modules/monitoring/application/forms/StatehistoryForm.php new file mode 100644 index 0000000..3a7c10d --- /dev/null +++ b/modules/monitoring/application/forms/StatehistoryForm.php @@ -0,0 +1,141 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Forms; + +use Icinga\Web\Form; +use Icinga\Data\Filter\Filter; + +/** + * Configure the filter for the event grid + */ +class StatehistoryForm extends Form +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setName('form_event_overview'); + $this->setSubmitLabel($this->translate('Apply')); + } + + /** + * Return the corresponding filter-object + * + * @returns Filter + */ + public function getFilter() + { + $baseFilter = Filter::matchAny( + Filter::expression('type', '=', 'hard_state') + ); + + if ($this->getValue('objecttype', 'hosts') === 'hosts') { + $objectTypeFilter = Filter::expression('object_type', '=', 'host'); + } else { + $objectTypeFilter = Filter::expression('object_type', '=', 'service'); + } + + $states = array( + 'cnt_down_hard' => Filter::expression('state', '=', '1'), + 'cnt_unreachable_hard' => Filter::expression('state', '=', '2'), + 'cnt_up' => Filter::expression('state', '=', '0'), + 'cnt_critical_hard' => Filter::expression('state', '=', '2'), + 'cnt_warning_hard' => Filter::expression('state', '=', '1'), + 'cnt_unknown_hard' => Filter::expression('state', '=', '3'), + 'cnt_ok' => Filter::expression('state', '=', '0') + ); + $state = $this->getValue('state', 'cnt_critical_hard'); + $stateFilter = $states[$state]; + if (in_array($state, array('cnt_ok', 'cnt_up'))) { + return Filter::matchAll($objectTypeFilter, $stateFilter); + } + return Filter::matchAll($baseFilter, $objectTypeFilter, $stateFilter); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $this->addElement( + 'select', + 'from', + array( + 'label' => $this->translate('From'), + 'value' => $this->getRequest()->getParam('from', strtotime('3 months ago')), + 'multiOptions' => array( + strtotime('midnight 3 months ago') => $this->translate('3 Months'), + strtotime('midnight 4 months ago') => $this->translate('4 Months'), + strtotime('midnight 8 months ago') => $this->translate('8 Months'), + strtotime('midnight 12 months ago') => $this->translate('1 Year'), + strtotime('midnight 24 months ago') => $this->translate('2 Years') + ) + ) + ); + $this->addElement( + 'select', + 'to', + array( + 'label' => $this->translate('To'), + 'value' => $this->getRequest()->getParam('to', time()), + 'multiOptions' => array( + time() => $this->translate('Today') + ) + ) + ); + + $objectType = $this->getRequest()->getParam('objecttype', 'services'); + $this->addElement( + 'select', + 'objecttype', + array( + 'label' => $this->translate('Object type'), + 'value' => $objectType, + 'multiOptions' => array( + 'services' => $this->translate('Services'), + 'hosts' => $this->translate('Hosts') + ) + ) + ); + if ($objectType === 'services') { + $serviceState = $this->getRequest()->getParam('state', 'cnt_critical_hard'); + if (in_array($serviceState, array('cnt_down_hard', 'cnt_unreachable_hard', 'cnt_up'))) { + $serviceState = 'cnt_critical_hard'; + } + $this->addElement( + 'select', + 'state', + array( + 'label' => $this->translate('State'), + 'value' => $serviceState, + 'multiOptions' => array( + 'cnt_critical_hard' => $this->translate('Critical'), + 'cnt_warning_hard' => $this->translate('Warning'), + 'cnt_unknown_hard' => $this->translate('Unknown'), + 'cnt_ok' => $this->translate('Ok') + ) + ) + ); + } else { + $hostState = $this->getRequest()->getParam('state', 'cnt_down_hard'); + if (in_array($hostState, array('cnt_ok', 'cnt_critical_hard', 'cnt_warning', 'cnt_unknown'))) { + $hostState = 'cnt_down_hard'; + } + $this->addElement( + 'select', + 'state', + array( + 'label' => $this->translate('State'), + 'value' => $hostState, + 'multiOptions' => array( + 'cnt_up' => $this->translate('Up'), + 'cnt_down_hard' => $this->translate('Down'), + 'cnt_unreachable_hard' => $this->translate('Unreachable') + ) + ) + ); + } + } +} diff --git a/modules/monitoring/application/views/helpers/CheckPerformance.php b/modules/monitoring/application/views/helpers/CheckPerformance.php new file mode 100644 index 0000000..feac4d8 --- /dev/null +++ b/modules/monitoring/application/views/helpers/CheckPerformance.php @@ -0,0 +1,50 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +/** + * Convert check summary data into a simple usable stdClass + */ +class Zend_View_Helper_CheckPerformance extends Zend_View_Helper_Abstract +{ + /** + * Create dispatch instance + * + * @return $this + */ + public function checkPerformance() + { + return $this; + } + + /** + * Create a condensed row of object data + * + * @param array $results Array of stdClass + * + * @return stdClass Condensed row + */ + public function create(array $results) + { + $out = new stdClass(); + $out->host_passive_count = 0; + $out->host_passive_latency_avg = 0; + $out->host_passive_execution_avg = 0; + $out->service_passive_count = 0; + $out->service_passive_latency_avg = 0; + $out->service_passive_execution_avg = 0; + $out->service_active_count = 0; + $out->service_active_latency_avg = 0; + $out->service_active_execution_avg = 0; + $out->host_active_count = 0; + $out->host_active_latency_avg = 0; + $out->host_active_execution_avg = 0; + + foreach ($results as $row) { + $key = $row->object_type . '_' . $row->check_type . '_'; + $out->{$key . 'count'} = $row->object_count; + $out->{$key . 'latency_avg'} = $row->latency / $row->object_count; + $out->{$key . 'execution_avg'} = $row->execution_time / $row->object_count; + } + return $out; + } +} diff --git a/modules/monitoring/application/views/helpers/ContactFlags.php b/modules/monitoring/application/views/helpers/ContactFlags.php new file mode 100644 index 0000000..858c726 --- /dev/null +++ b/modules/monitoring/application/views/helpers/ContactFlags.php @@ -0,0 +1,46 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +class Zend_View_Helper_ContactFlags extends Zend_View_Helper_Abstract +{ + /** + * Get the human readable flag name for the given contact notification option + * + * @param string $tableName The name of the option table + * + * @return string + */ + public function getNotificationOptionName($tableName) + { + $exploded = explode('_', $tableName); + $name = end($exploded); + return ucfirst($name); + } + + /** + * Build all active notification options to a readable string + * + * @param object $contact The contact retrieved from a backend + * @param string $type Whether to display the flags for 'host' or 'service' + * @param string $glue The symbol to use to concatenate the flag names + * + * @return string A string that contains a human readable list of active options + */ + public function contactFlags($contact, $type, $glue = ', ') + { + $optionName = 'contact_' . $type . '_notification_options'; + if (isset($contact->$optionName)) { + return $contact->$optionName; + } + $out = array(); + foreach ($contact as $key => $value) { + if (preg_match('/^contact_notify_' . $type . '_.*/', $key) && $value == true) { + $option = $this->getNotificationOptionName($key); + if (strtolower($option) != 'timeperiod') { + array_push($out, $option); + } + } + } + return implode($glue, $out); + } +} diff --git a/modules/monitoring/application/views/helpers/Customvar.php b/modules/monitoring/application/views/helpers/Customvar.php new file mode 100644 index 0000000..8bfdc3a --- /dev/null +++ b/modules/monitoring/application/views/helpers/Customvar.php @@ -0,0 +1,62 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +class Zend_View_Helper_Customvar extends Zend_View_Helper_Abstract +{ + /** + * Create dispatch instance + * + * @return $this + */ + public function checkPerformance() + { + return $this; + } + + public function customvar($struct) + { + if (is_scalar($struct)) { + return nl2br($this->view->escape( + is_string($struct) + ? $struct + : var_export($struct, true) + ), false); + } elseif (is_array($struct)) { + return $this->renderArray($struct); + } elseif (is_object($struct)) { + return $this->renderObject($struct); + } + } + + protected function renderArray($array) + { + if (empty($array)) { + return '[]'; + } + $out = "<ul>\n"; + + foreach ($array as $val) { + $out .= '<li>' . $this->customvar($val) . "</li>\n"; + } + + return $out . "</ul>\n"; + } + + protected function renderObject($object) + { + if (0 === count((array) $object)) { + return '{}'; + } + $out = "{<ul>\n"; + + foreach ($object as $key => $val) { + $out .= '<li>' + . $this->view->escape($key) + . ' => ' + . $this->customvar($val) + . "</li>\n"; + } + + return $out . "</ul>}"; + } +} diff --git a/modules/monitoring/application/views/helpers/EscapeComment.php b/modules/monitoring/application/views/helpers/EscapeComment.php new file mode 100644 index 0000000..be85a22 --- /dev/null +++ b/modules/monitoring/application/views/helpers/EscapeComment.php @@ -0,0 +1,38 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +/** + * Helper for escaping comments, but preserving links + */ +class Zend_View_Helper_EscapeComment extends Zend_View_Helper_Abstract +{ + /** + * The purifier to use for escaping + * + * @var HTMLPurifier + */ + protected static $purifier; + + /** + * Escape any comment for being placed inside HTML, but preserve simple links (<a href="...">). + * + * @param string $comment + * + * @return string + */ + public function escapeComment($comment) + { + if (self::$purifier === null) { + require_once 'HTMLPurifier/Bootstrap.php'; + require_once 'HTMLPurifier.php'; + require_once 'HTMLPurifier.autoload.php'; + + $config = HTMLPurifier_Config::createDefault(); + $config->set('Core.EscapeNonASCIICharacters', true); + $config->set('HTML.Allowed', 'a[href]'); + $config->set('Cache.DefinitionImpl', null); + self::$purifier = new HTMLPurifier($config); + } + return self::$purifier->purify($comment); + } +} diff --git a/modules/monitoring/application/views/helpers/HostFlags.php b/modules/monitoring/application/views/helpers/HostFlags.php new file mode 100644 index 0000000..81d8ebc --- /dev/null +++ b/modules/monitoring/application/views/helpers/HostFlags.php @@ -0,0 +1,33 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +class Zend_View_Helper_HostFlags extends Zend_View_Helper_Abstract +{ + public function hostFlags($host) + { + $icons = array(); + if (! $host->host_handled && $host->host_state > 0) { + $icons[] = $this->view->icon('attention-alt', $this->view->translate('Unhandled')); + } + if ($host->host_acknowledged) { + $icons[] = $this->view->icon('ok', $this->view->translate('Acknowledged')); + } + if ($host->host_is_flapping) { + $icons[] = $this->view->icon('flapping', $this->view->translate('Flapping')); + } + if (! $host->host_notifications_enabled) { + $icons[] = $this->view->icon('bell-off-empty', $this->view->translate('Notifications Disabled')); + } + if ($host->host_in_downtime) { + $icons[] = $this->view->icon('plug', $this->view->translate('In Downtime')); + } + if (! $host->host_active_checks_enabled) { + if (! $host->host_passive_checks_enabled) { + $icons[] = $this->view->icon('eye-off', $this->view->translate('Active And Passive Checks Disabled')); + } else { + $icons[] = $this->view->icon('eye-off', $this->view->translate('Active Checks Disabled')); + } + } + return implode(' ', $icons); + } +} diff --git a/modules/monitoring/application/views/helpers/IconImage.php b/modules/monitoring/application/views/helpers/IconImage.php new file mode 100644 index 0000000..3c0ca43 --- /dev/null +++ b/modules/monitoring/application/views/helpers/IconImage.php @@ -0,0 +1,64 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +use Icinga\Module\Monitoring\Object\Macro; + +/** + * Generate icons to describe a given hosts state + */ +class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract +{ + /** + * Create dispatch instance + * + * @return \Zend_View_Helper_IconImage + */ + public function iconImage() + { + return $this; + } + + /** + * Display the image_icon of a MonitoredObject + * + * @param MonitoredObject|stdClass $object The host or service + * @return string + */ + public function host($object) + { + if ($object->host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) { + return $this->view->icon( + Macro::resolveMacros($object->host_icon_image, $object), + null, + array( + 'alt' => $object->host_icon_image_alt, + 'class' => 'host-icon-image', + 'title' => $object->host_icon_image_alt + ) + ); + } + return ''; + } + + /** + * Display the image_icon of a MonitoredObject + * + * @param MonitoredObject|stdClass $object The host or service + * @return string + */ + public function service($object) + { + if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) { + return $this->view->icon( + Macro::resolveMacros($object->service_icon_image, $object), + null, + array( + 'alt' => $object->service_icon_image_alt, + 'class' => 'service-icon-image', + 'title' => $object->service_icon_image_alt + ) + ); + } + return ''; + } +} diff --git a/modules/monitoring/application/views/helpers/Link.php b/modules/monitoring/application/views/helpers/Link.php new file mode 100644 index 0000000..c5443a4 --- /dev/null +++ b/modules/monitoring/application/views/helpers/Link.php @@ -0,0 +1,72 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +/** + * Helper for generating frequently used jump links + * + * Most of the monitoring overviews link to detail information, e.g. the full information of the involved monitored + * object. Instead of reintroducing link generation and translation in those views, this helper contains most + * frequently used jump links. + */ +class Zend_View_Helper_Link extends Zend_View_Helper_Abstract +{ + /** + * Helper entry point + * + * @return $this + */ + public function link() + { + return $this; + } + + /** + * Create a host link + * + * @param string $host Hostname + * @param string $linkText Link text, e.g. the host's display name + * + * @return string + */ + public function host($host, $linkText) + { + return $this->view->qlink( + $linkText, + 'monitoring/host/show', + array('host' => $host), + array('title' => sprintf($this->view->translate('Show detailed information for host %s'), $linkText)) + ); + } + + /** + * Create a service link + * + * @param string $service Service name + * @param string $serviceLinkText Text for the service link, e.g. the service's display name + * @param string $host Hostname + * @param string $hostLinkText Text for the host link, e.g. the host's display name + * @param string $class An optional class to use for this link + * + * @return string + */ + public function service($service, $serviceLinkText, $host, $hostLinkText, $class = null) + { + return sprintf( + '%s: %s', + $this->host($host, $hostLinkText), + $this->view->qlink( + $serviceLinkText, + 'monitoring/service/show', + array('host' => $host, 'service' => $service), + array( + 'title' => sprintf( + $this->view->translate('Show detailed information for service %s on host %s'), + $serviceLinkText, + $hostLinkText + ), + 'class' => $class + ) + ) + ); + } +} diff --git a/modules/monitoring/application/views/helpers/MonitoringFlags.php b/modules/monitoring/application/views/helpers/MonitoringFlags.php new file mode 100644 index 0000000..8509e5b --- /dev/null +++ b/modules/monitoring/application/views/helpers/MonitoringFlags.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +/* use Icinga\Module\Monitoring\Object\MonitoredObject; */ + +/** + * Rendering helper for object's properties which may be either enabled or disabled + */ +class Zend_View_Helper_MonitoringFlags extends Zend_View_Helper_Abstract +{ + /** + * Object's properties which may be either enabled or disabled and their human readable description + * + * @var string[] + */ + private static $flags = array( + 'passive_checks_enabled' => 'Passive Checks', + 'active_checks_enabled' => 'Active Checks', + 'obsessing' => 'Obsessing', + 'notifications_enabled' => 'Notifications', + 'event_handler_enabled' => 'Event Handler', + 'flap_detection_enabled' => 'Flap Detection', + ); + + /** + * Retrieve flags as array with either true or false as value + * + * @param MonitoredObject $object + * + * @return array + */ + public function monitoringFlags(/*MonitoredObject*/ $object) + { + $flags = array(); + foreach (self::$flags as $column => $description) { + $flags[$description] = (bool) $object->{$column}; + } + return $flags; + } +} diff --git a/modules/monitoring/application/views/helpers/Perfdata.php b/modules/monitoring/application/views/helpers/Perfdata.php new file mode 100644 index 0000000..e7bc72b --- /dev/null +++ b/modules/monitoring/application/views/helpers/Perfdata.php @@ -0,0 +1,116 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +use Icinga\Module\Monitoring\Plugin\Perfdata; +use Icinga\Module\Monitoring\Plugin\PerfdataSet; +use Icinga\Util\StringHelper; + +class Zend_View_Helper_Perfdata extends Zend_View_Helper_Abstract +{ + /** + * Display the given perfdata string to the user + * + * @param string $perfdataStr The perfdata string + * @param bool $compact Whether to display the perfdata in compact mode + * @param int $limit Max labels to show; 0 for no limit + * @param string $color The color indicating the perfdata state + * + * @return string + */ + public function perfdata($perfdataStr, $compact = false, $limit = 0, $color = Perfdata::PERFDATA_OK) + { + $pieChartData = PerfdataSet::fromString($perfdataStr)->asArray(); + uasort( + $pieChartData, + function ($a, $b) { + return $a->worseThan($b) ? -1 : ($b->worseThan($a) ? 1 : 0); + } + ); + $results = array(); + $keys = array('', 'label', 'value', 'min', 'max', 'warn', 'crit'); + $columns = array(); + $labels = array_combine( + $keys, + array( + '', + $this->view->translate('Label'), + $this->view->translate('Value'), + $this->view->translate('Min'), + $this->view->translate('Max'), + $this->view->translate('Warning'), + $this->view->translate('Critical') + ) + ); + foreach ($pieChartData as $perfdata) { + if ($perfdata->isVisualizable()) { + $columns[''] = ''; + } + foreach ($perfdata->toArray() as $column => $value) { + if (empty($value) || + $column === 'min' && floatval($value) === 0.0 || + $column === 'max' && $perfdata->isPercentage() && floatval($value) === 100) { + continue; + } + $columns[$column] = $labels[$column]; + } + } + // restore original column array sorting + $headers = array(); + foreach ($keys as $column) { + if (isset($columns[$column])) { + $headers[$column] = $labels[$column]; + } + } + $table = array('<thead><tr><th>' . implode('</th><th>', $headers) . '</th></tr></thead><tbody>'); + foreach ($pieChartData as $perfdata) { + if ($compact && $perfdata->isVisualizable()) { + $results[] = $perfdata->asInlinePie($color)->render(); + } else { + $data = array(); + if ($perfdata->isVisualizable()) { + $data []= $perfdata->asInlinePie($color)->render(); + } elseif (isset($columns[''])) { + $data []= ''; + } + if (! $compact) { + foreach ($perfdata->toArray() as $column => $value) { + if (! isset($columns[$column])) { + continue; + } + $text = $this->view->escape(empty($value) ? '-' : $value); + $data []= sprintf( + '<span title="%s">%s</span>', + $text, + $text + ); + } + } + $table []= '<tr><td class="sparkline-col">' . implode('</td><td>', $data) . '</td></tr>'; + } + } + $table[] = '</tbody>'; + if ($limit > 0) { + $count = $compact ? count($results) : count($table); + if ($count > $limit) { + if ($compact) { + $results = array_slice($results, 0, $limit); + $title = sprintf($this->view->translate('%d more ...'), $count - $limit); + $results[] = '<span aria-hidden="true" title="' . $title . '">...</span>'; + } else { + $table = array_slice($table, 0, $limit); + } + } + } + if ($compact) { + return join('', $results); + } else { + if (empty($table)) { + return ''; + } + return sprintf( + '<table class="performance-data-table collapsible" data-visible-rows="6">%s</table>', + implode("\n", $table) + ); + } + } +} diff --git a/modules/monitoring/application/views/helpers/PluginOutput.php b/modules/monitoring/application/views/helpers/PluginOutput.php new file mode 100644 index 0000000..bcd3d9e --- /dev/null +++ b/modules/monitoring/application/views/helpers/PluginOutput.php @@ -0,0 +1,199 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +use Icinga\Web\Dom\DomNodeIterator; +use Icinga\Web\View; +use Icinga\Web\Helper\HtmlPurifier; + +/** + * Plugin output renderer + */ +class Zend_View_Helper_PluginOutput extends Zend_View_Helper_Abstract +{ + /** + * Patterns to be replaced in plain text plugin output + * + * @var array + */ + protected static $txtPatterns = array( + '~\\\t~', + '~\\\n~', + '~(\[|\()OK(\]|\))~', + '~(\[|\()WARNING(\]|\))~', + '~(\[|\()CRITICAL(\]|\))~', + '~(\[|\()UNKNOWN(\]|\))~', + '~(\[|\()UP(\]|\))~', + '~(\[|\()DOWN(\]|\))~', + '~\@{6,}~' + ); + + /** + * Replacements for $txtPatterns + * + * @var array + */ + protected static $txtReplacements = array( + "\t", + "\n", + '<span class="state-ok">$1OK$2</span>', + '<span class="state-warning">$1WARNING$2</span>', + '<span class="state-critical">$1CRITICAL$2</span>', + '<span class="state-unknown">$1UNKNOWN$2</span>', + '<span class="state-up">$1UP$2</span>', + '<span class="state-down">$1DOWN$2</span>', + '@@@@@@', + ); + + /** + * Patterns to be replaced in html plugin output + * + * @var array + */ + protected static $htmlPatterns = array( + '~\\\t~', + '~\\\n~', + '~<table~' + ); + + /** + * Replacements for $htmlPatterns + * + * @var array + */ + protected static $htmlReplacements = array( + "\t", + "\n", + '<table style="font-size: 0.75em"' + ); + + /** @var \Icinga\Module\Monitoring\Web\Helper\PluginOutputHookRenderer */ + protected $hookRenderer; + + public function __construct() + { + $this->hookRenderer = (new \Icinga\Module\Monitoring\Web\Helper\PluginOutputHookRenderer())->registerHooks(); + } + + /** + * Render plugin output + * + * @param string $output + * @param bool $raw + * @param string $command Check command + * + * @return string + */ + public function pluginOutput($output, $raw = false, $command = null) + { + if (empty($output)) { + return ''; + } + + if ($command !== null) { + $output = $this->hookRenderer->render($command, $output, ! $raw); + } + + if (preg_match('~<\w+(?>\s\w+=[^>]*)?>~', $output)) { + // HTML + $output = HtmlPurifier::process(preg_replace( + self::$htmlPatterns, + self::$htmlReplacements, + $output + )); + $isHtml = true; + } else { + // Plaintext + $output = preg_replace( + self::$txtPatterns, + self::$txtReplacements, + // Not using the view here to escape this. The view sets `double_encode` to true + htmlspecialchars($output, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, View::CHARSET, false) + ); + $isHtml = false; + } + + $output = trim($output); + // Add zero-width space after commas which are not followed by a whitespace character + // in oder to help browsers to break words in plugin output + $output = preg_replace('/,(?=[^\s])/', ',​', $output); + if (! $raw) { + if ($isHtml) { + $output = $this->processHtml($output); + $output = '<div class="plugin-output">' . $output . '</div>'; + } else { + $output = '<div class="plugin-output preformatted">' . $output . '</div>'; + } + } + + return $output; + } + + /** + * Replace classic Icinga CGI links with Icinga Web 2 links and color state information, if any + * + * @param string $html + * + * @return string + */ + protected function processHtml($html) + { + $pattern = '/[([](OK|WARNING|CRITICAL|UNKNOWN|UP|DOWN)[)\]]/'; + $doc = new DOMDocument(); + $doc->loadXML('<div>' . $html . '</div>', LIBXML_NOERROR | LIBXML_NOWARNING); + $dom = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST); + $nodesToRemove = array(); + foreach ($dom as $node) { + /** @var \DOMNode $node */ + if ($node->nodeType === XML_TEXT_NODE) { + $start = 0; + while (preg_match($pattern, $node->nodeValue, $match, PREG_OFFSET_CAPTURE, $start)) { + $offsetLeft = $match[0][1]; + $matchLength = strlen($match[0][0]); + $leftLength = $offsetLeft - $start; + // if there is text before the match + if ($leftLength) { + // create node for leading text + $text = new DOMText(substr($node->nodeValue, $start, $leftLength)); + $node->parentNode->insertBefore($text, $node); + } + // create the new element for the match + $span = $doc->createElement('span', $match[0][0]); + $span->setAttribute('class', 'state-' . strtolower($match[1][0])); + $node->parentNode->insertBefore($span, $node); + + // start for next match + $start = $offsetLeft + $matchLength; + } + if ($start) { + // is there text left? + if (strlen($node->nodeValue) > $start) { + // create node for trailing text + $text = new DOMText(substr($node->nodeValue, $start)); + $node->parentNode->insertBefore($text, $node); + } + // delete the old node later + $nodesToRemove[] = $node; + } + } elseif ($node->nodeType === XML_ELEMENT_NODE) { + /** @var \DOMElement $node */ + if ($node->tagName === 'a' + && preg_match('~^/cgi\-bin/status\.cgi\?(.+)$~', $node->getAttribute('href'), $match) + ) { + parse_str($match[1], $params); + if (isset($params['host'])) { + $node->setAttribute( + 'href', + $this->view->baseUrl('/monitoring/host/show?host=' . urlencode($params['host'])) + ); + } + } + } + } + foreach ($nodesToRemove as $node) { + /** @var \DOMNode $node */ + $node->parentNode->removeChild($node); + } + + return substr($doc->saveHTML(), 5, -7); + } +} diff --git a/modules/monitoring/application/views/helpers/RuntimeVariables.php b/modules/monitoring/application/views/helpers/RuntimeVariables.php new file mode 100644 index 0000000..e80e8aa --- /dev/null +++ b/modules/monitoring/application/views/helpers/RuntimeVariables.php @@ -0,0 +1,50 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +/** + * Convert runtime summary data into a simple usable stdClass + */ +class Zend_View_Helper_RuntimeVariables extends Zend_View_Helper_Abstract +{ + /** + * Create dispatch instance + * + * @return $this + */ + public function runtimeVariables() + { + return $this; + } + + /** + * Create a condensed row of object data + * + * @param $result stdClass + * + * @return stdClass Condensed row + */ + public function create(stdClass $result) + { + $out = new stdClass(); + $out->total_hosts = isset($result->total_hosts) + ? $result->total_hosts + : 0; + $out->total_scheduled_hosts = isset($result->total_scheduled_hosts) + ? $result->total_scheduled_hosts + : 0; + $out->total_services = isset($result->total_services) + ? $result->total_services + : 0; + $out->total_scheduled_services = isset($result->total_scheduled_services) + ? $result->total_scheduled_services + : 0; + $out->average_services_per_host = $out->total_hosts > 0 + ? $out->total_services / $out->total_hosts + : 0; + $out->average_scheduled_services_per_host = $out->total_scheduled_hosts > 0 + ? $out->total_scheduled_services / $out->total_scheduled_hosts + : 0; + + return $out; + } +} diff --git a/modules/monitoring/application/views/helpers/ServiceFlags.php b/modules/monitoring/application/views/helpers/ServiceFlags.php new file mode 100644 index 0000000..47a351c --- /dev/null +++ b/modules/monitoring/application/views/helpers/ServiceFlags.php @@ -0,0 +1,33 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +class Zend_View_Helper_ServiceFlags extends Zend_View_Helper_Abstract +{ + public function serviceFlags($service) + { + $icons = array(); + if (! $service->service_handled && $service->service_state > 0) { + $icons[] = $this->view->icon('attention-alt', $this->view->translate('Unhandled')); + } + if ($service->service_acknowledged) { + $icons[] = $this->view->icon('ok', $this->view->translate('Acknowledged')); + } + if ($service->service_is_flapping) { + $icons[] = $this->view->icon('flapping', $this->view->translate('Flapping')); + } + if (! $service->service_notifications_enabled) { + $icons[] = $this->view->icon('bell-off-empty', $this->view->translate('Notifications Disabled')); + } + if ($service->service_in_downtime) { + $icons[] = $this->view->icon('plug', $this->view->translate('In Downtime')); + } + if (! $service->service_active_checks_enabled) { + if (! $service->service_passive_checks_enabled) { + $icons[] = $this->view->icon('eye-off', $this->view->translate('Active And Passive Checks Disabled')); + } else { + $icons[] = $this->view->icon('eye-off', $this->view->translate('Active Checks Disabled')); + } + } + return implode(' ', $icons); + } +} diff --git a/modules/monitoring/application/views/scripts/comment/remove.phtml b/modules/monitoring/application/views/scripts/comment/remove.phtml new file mode 100644 index 0000000..73f8c68 --- /dev/null +++ b/modules/monitoring/application/views/scripts/comment/remove.phtml @@ -0,0 +1,11 @@ +<div class="controls"> + + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + + <?= $this->render('partials/downtime/downtime-header.phtml'); ?> +</div> +<div class="content object-command"> + <?= $delDowntimeForm; ?> +</div> diff --git a/modules/monitoring/application/views/scripts/comment/show.phtml b/modules/monitoring/application/views/scripts/comment/show.phtml new file mode 100644 index 0000000..3cbfb76 --- /dev/null +++ b/modules/monitoring/application/views/scripts/comment/show.phtml @@ -0,0 +1,86 @@ +<div class="controls"> + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + + <div data-base-target='_next'> + <?= $this->render('partials/comment/comment-header.phtml'); ?> + </div> +</div> +<div class="content"> + +<h2><?= $this->translate('Comment detail information') ?></h2> +<table class="name-value-table"> + <tbody> + <tr> + <?php if ($this->comment->objecttype === 'service'): ?> + <th> <?= $this->translate('Service') ?> </th> + <td> + <?= $this->icon('service', $this->translate('Service')); ?> + <?= $this->link()->service( + $this->comment->service_description, + $this->comment->service_display_name, + $this->comment->host_name, + $this->comment->host_display_name + ); + ?> + </td> + <?php else: ?> + <th> <?= $this->translate('Host') ?> </th> + <td> + <?= $this->icon('host', $this->translate('Host')); ?> + <?= $this->link()->host( + $this->comment->host_name, + $this->comment->host_display_name + ); + ?> + </td> + <?php endif ?> + </tr> + + <tr> + <th><?= $this->translate('Author') ?></th> + <td><?= $this->icon('user', $this->translate('User')) ?> <?= $this->escape($this->comment->author) ?></td> + </tr> + + <tr> + <th><?= $this->translate('Persistent') ?></th> + <td><?= $this->escape($this->comment->persistent) ? $this->translate('Yes') : $this->translate('No') ?></td> + </tr> + + <tr> + <th><?= $this->translate('Created') ?></th> + <td><?= $this->formatDateTime($this->comment->timestamp) ?></td> + </tr> + + <tr> + <th><?= $this->translate('Expires') ?></th> + <td> + <?= $this->comment->expiration ? sprintf( + $this->translate('This comment expires on %s at %s.'), + $this->formatDate($this->comment->expiration), + $this->formatTime($this->comment->expiration) + ) : $this->translate('This comment does not expire.'); + ?> + </td> + </tr> + + <tr> + <th><?= $this->translate('Comment') ?></th> + <td><?= $this->nl2br($this->createTicketLinks($this->markdown($comment->comment))) ?></td> + </tr> + + <?php if (isset($delCommentForm)): // Form is unset if the current user lacks the respective permission ?> + <tr class="newsection"> + <th><?= $this->translate('Commands') ?></th> + <td> + <?= $delCommentForm ?> + </td> + </tr> + <?php endif ?> + + </tbody> +</table> + +</div> + diff --git a/modules/monitoring/application/views/scripts/comments/delete-all.phtml b/modules/monitoring/application/views/scripts/comments/delete-all.phtml new file mode 100644 index 0000000..698c4ee --- /dev/null +++ b/modules/monitoring/application/views/scripts/comments/delete-all.phtml @@ -0,0 +1,12 @@ +<div class="controls"> + + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + + <?= $this->render('partials/comment/comments-header.phtml'); ?> +</div> + +<div class="content object-command"> + <?= $delCommentForm ?> +</div> diff --git a/modules/monitoring/application/views/scripts/comments/show.phtml b/modules/monitoring/application/views/scripts/comments/show.phtml new file mode 100644 index 0000000..67e1c6b --- /dev/null +++ b/modules/monitoring/application/views/scripts/comments/show.phtml @@ -0,0 +1,19 @@ +<div class="controls"> +<?php if (! $this->compact): ?> + <?= $this->tabs ?> +<?php endif ?> + <?= $this->render('partials/comment/comments-header.phtml') ?> +</div> + +<div class="content multi-commands"> + <h2><?= $this->translate('Commands') ?></h2> + <?= $this->qlink( + sprintf($this->translate('Remove %d comments'), $comments->count()), + $removeAllLink, + null, + array( + 'icon' => 'trash', + 'title' => $this->translate('Remove all selected comments') + ) + ) ?> +</div> diff --git a/modules/monitoring/application/views/scripts/config/form.phtml b/modules/monitoring/application/views/scripts/config/form.phtml new file mode 100644 index 0000000..cbf0659 --- /dev/null +++ b/modules/monitoring/application/views/scripts/config/form.phtml @@ -0,0 +1,6 @@ +<div class="controls"> + <?= $tabs->showOnlyCloseButton(); ?> +</div> +<div class="content"> + <?= $form; ?> +</div>
\ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/config/index.phtml b/modules/monitoring/application/views/scripts/config/index.phtml new file mode 100644 index 0000000..a1264c2 --- /dev/null +++ b/modules/monitoring/application/views/scripts/config/index.phtml @@ -0,0 +1,78 @@ +<div class="controls"> + <?= $tabs ?> +</div> + +<div class="content" data-base-target="_next"> + <div> + <h2><?= $this->translate('Monitoring Backends') ?></h2> + <?= $this->qlink( + $this->translate('Create a New Monitoring Backend') , + 'monitoring/config/createbackend', + null, + array( + 'class' => 'button-link', + 'icon' => 'plus', + 'title' => $this->translate('Create a new monitoring backend') + ) + ) ?> + <table class="table-row-selectable common-table"> + <thead> + <tr> + <th><?= $this->translate('Monitoring Backend') ?></th> + <th></th> + </tr> + </thead> + <tbody> + <?php foreach ($this->backendsConfig as $backendName => $config): ?> + <tr> + <td> + <?= $this->qlink( + $backendName, + 'monitoring/config/editbackend', + array('backend-name' => $backendName), + array( + 'icon' => 'edit', + 'title' => sprintf($this->translate('Edit monitoring backend %s'), $backendName) + ) + ) ?> + <span class="config-label-meta">(<?= sprintf( + $this->translate('Type: %s'), + $this->escape($config->type === 'ido' ? 'IDO' : ucfirst($config->type)) + ) ?>) + </span> + </td> + <td class="text-right"> + <?= $this->qlink( + '', + 'monitoring/config/removebackend', + array('backend-name' => $backendName), + array( + 'class' => 'action-link', + 'icon' => 'cancel', + 'title' => sprintf($this->translate('Remove monitoring backend %s'), $backendName) + ) + ) ?> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> + </div> + <div> + <h2><?= $this->translate('Command Transports') ?></h2> + <?= $this->qlink( + $this->translate('Create a New Command Transport') , + 'monitoring/config/createtransport', + null, + array( + 'class' => 'button-link', + 'icon' => 'plus', + 'title' => $this->translate('Create a new command transport') + ) + ) ?> + <?php + /** @var \Icinga\Module\Monitoring\Forms\Config\TransportReorderForm $commandTransportReorderForm */ + echo $commandTransportReorderForm; + ?> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/config/security.phtml b/modules/monitoring/application/views/scripts/config/security.phtml new file mode 100644 index 0000000..3801678 --- /dev/null +++ b/modules/monitoring/application/views/scripts/config/security.phtml @@ -0,0 +1,6 @@ +<div class="controls"> + <?= $tabs; ?> +</div> +<div class="content"> + <?= $form; ?> +</div>
\ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/downtime/remove.phtml b/modules/monitoring/application/views/scripts/downtime/remove.phtml new file mode 100644 index 0000000..34a7dbd --- /dev/null +++ b/modules/monitoring/application/views/scripts/downtime/remove.phtml @@ -0,0 +1,13 @@ +<div class="controls"> + + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + + <table> + <tr> <?= $this->render('partials/downtime/downtime-header.phtml') ?> </tr> + </table> +</div> +<div class="content object-command"> + <?= $delDowntimeForm; ?> +</div>
\ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/downtime/show.phtml b/modules/monitoring/application/views/scripts/downtime/show.phtml new file mode 100644 index 0000000..4db03cb --- /dev/null +++ b/modules/monitoring/application/views/scripts/downtime/show.phtml @@ -0,0 +1,173 @@ +<div class="controls"> + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + + <table> + <tr> <?= $this->render('partials/downtime/downtime-header.phtml'); ?> </tr> + </table> +</div> +<div class="content"><h2><?= $this->translate('Details') ?></h2> + <table class="name-value-table"> + <tbody> + <tr> + <th> + <?= $this->isService ? $this->translate('Service') : $this->translate('Host') ?> + </th> + <td data-base-target="_next"> + <?php if ($this->isService): ?> + <?php + $link = $this->link()->service( + $downtime->service_description, + $downtime->service_display_name, + $downtime->host_name, + $downtime->host_display_name + ); + $icon = $this->icon('service', $this->translate('Service')); + ?> + <?php else: ?> + <?php + $icon = $this->icon('host', $this->translate('Host')); + $link = $this->link()->host($downtime->host_name, $downtime->host_display_name) + ?> + <?php endif ?> + <?= $icon ?> + <?= $link ?> + </td> + </tr> + <tr title="<?= $this->translate('The name of the person who scheduled this downtime'); ?>"> + <th><?= $this->translate('Author') ?></th> + <td><?= $this->icon('user', $this->translate('User')) ?> <?= $this->escape($this->downtime->author_name) ?></td> + </tr> + <tr title="<?= $this->translate('Date and time this downtime was entered'); ?>"> + <th><?= $this->translate('Entry Time') ?></th> + <td><?= $this->formatDateTime($this->downtime->entry_time) ?></td> + </tr> + <tr title="<?= $this->translate('A comment, as entered by the author, associated with the scheduled downtime'); ?>"> + <th><?= $this->translate('Comment') ?></th> + <td><?= $this->nl2br($this->createTicketLinks($this->markdown($downtime->comment))) ?></td> + </tr> + </tbody> + </table> + + <h2> <?= $this->translate('Duration') ?> </h2> + + <table class="name-value-table"> + <tbody> + <tr class="newsection"> + <th><?= $this->escape( + $this->downtime->is_flexible ? + $this->translate('Flexible') : $this->translate('Fixed') + ); ?> + <?= $this->icon('info-circled', $this->downtime->is_flexible ? + $this->translate('Flexible downtimes have a hard start and end time,' + . ' but also an additional restriction on the duration in which ' + . ' the host or service may actually be down.') : + $this->translate('Fixed downtimes have a static start and end time.')) ?> + </th> + <td> + <?php if ($downtime->is_flexible): ?> + <?php if ($downtime->is_in_effect): ?> + <?= sprintf( + $isService + ? $this->translate('This flexible service downtime was started on %s at %s and lasts for %s until %s at %s.') + : $this->translate('This flexible host downtime was started on %s at %s and lasts for %s until %s at %s.'), + $this->formatDate($downtime->start), + $this->formatTime($downtime->start), + $this->formatDuration($downtime->duration), + $this->formatDate($downtime->end), + $this->formatTime($downtime->end) + ) ?> + <?php else: ?> + <?= sprintf( + $isService + ? $this->translate('This flexible service downtime has been scheduled to start between %s - %s and to last for %s.') + : $this->translate('This flexible host downtime has been scheduled to start between %s - %s and to last for %s.'), + $this->formatDateTime($downtime->scheduled_start), + $this->formatDateTime($downtime->scheduled_end), + $this->formatDuration($downtime->duration) + ) ?> + <?php endif ?> + <?php else: ?> + <?php if ($downtime->is_in_effect): ?> + <?= sprintf( + $isService + ? $this->translate('This fixed service downtime was started on %s at %s and expires on %s at %s.') + : $this->translate('This fixed host downtime was started on %s at %s and expires on %s at %s.'), + $this->formatDate($downtime->start), + $this->formatTime($downtime->start), + $this->formatDate($downtime->end), + $this->formatTime($downtime->end) + ) ?> + <?php else: ?> + <?= sprintf( + $isService + ? $this->translate('This fixed service downtime has been scheduled to start on %s at %s and to end on %s at %s.') + : $this->translate('This fixed host downtime has been scheduled to start on %s at %s and to end on %s at %s.'), + $this->formatDate($downtime->start), + $this->formatTime($downtime->start), + $this->formatDate($downtime->end), + $this->formatTime($downtime->end) + ) ?> + <?php endif ?> + <?php endif ?> + </td> + </tr> + <tr title="<?= $this->translate('The date/time the scheduled downtime is' + . ' supposed to start. If this is a flexible (non-fixed) downtime, ' + . 'this refers to the earliest possible time that the downtime' + . ' can start'); ?>"> + <th><?= $this->translate('Scheduled start') ?></th> + <td><?= $this->formatDateTime($this->downtime->scheduled_start) ?></td> + </tr> + <tr title="<?= $this->translate('The date/time the scheduled downtime is ' + . 'supposed to end. If this is a flexible (non-fixed) downtime, ' + . 'this refers to the last possible time that the downtime can ' + . 'start'); ?>"> + <th><?= $this->translate('Scheduled end') ?></th> + <td><?= $this->formatDateTime($this->downtime->scheduled_end) ?></td> + </tr> + <?php if ($this->downtime->is_flexible): ?> + <tr title="<?= $this->translate('Indicates the number of seconds that the ' + . 'scheduled downtime should last. This is usually only needed if' + . ' this is a flexible downtime, which can start at a variable ' + . 'time, but lasts for the specified duration'); ?>"> + <th tit><?= $this->translate('Duration') ?></th> + <td><?= $this->formatDuration($this->downtime->duration) ?></td> + </tr> + <tr title="<?= $this->translate('he date/time the scheduled downtime was' + . ' actually started'); ?>"> + <th><?= $this->translate('Actual start time') ?></th> + <td><?= $this->formatDateTime($downtime->start) ?></td> + </tr> + <tr title="<?= $this->translate('The date/time the scheduled downtime ' + . 'actually ended'); ?>"> + <th><?= $this->translate('Actual end time') ?></th> + <td><?= $this->formatDateTime($downtime->end) ?></td> + </tr> + <?php endif; ?> + + <tr class="newsection"> + <th><?= $this->translate('In effect') ?></th> + <td> + <?= $this->escape( + $this->downtime->is_in_effect ? + $this->translate('Yes') : $this->translate('No') + ); + ?> + </td> + </tr> + + <?php if (isset($delDowntimeForm)): // Form is unset if the current user lacks the respective permission ?> + <tr class="newsection"> + <th><?= $this->translate('Commands') ?></th> + <td> + <?= $delDowntimeForm ?> + </td> + </tr> + <?php endif ?> + </tbody> + </table> + +</div> + diff --git a/modules/monitoring/application/views/scripts/downtimes/delete-all.phtml b/modules/monitoring/application/views/scripts/downtimes/delete-all.phtml new file mode 100644 index 0000000..e6435fe --- /dev/null +++ b/modules/monitoring/application/views/scripts/downtimes/delete-all.phtml @@ -0,0 +1,12 @@ +<div class="controls"> + + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + + <?= $this->render('partials/downtime/downtimes-header.phtml'); ?> +</div> + +<div class="content object-command"> + <?= $delAllDowntimeForm ?> +</div>
\ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/downtimes/show.phtml b/modules/monitoring/application/views/scripts/downtimes/show.phtml new file mode 100644 index 0000000..73d9bf6 --- /dev/null +++ b/modules/monitoring/application/views/scripts/downtimes/show.phtml @@ -0,0 +1,19 @@ +<div class="controls"> +<?php if (! $this->compact): ?> + <?= $this->tabs ?> +<?php endif ?> + <?= $this->render('partials/downtime/downtimes-header.phtml') ?> +</div> + +<div class="content multi-commands"> + <h2> <?= $this->translate('Commands') ?> </h2> + <?= $this->qlink( + sprintf($this->translate('Remove all %d scheduled downtimes'), $downtimes->count()), + $removeAllLink, + null, + array( + 'icon' => 'trash', + 'title' => $this->translate('Remove all selected downtimes') + ) + ) ?> +</div> diff --git a/modules/monitoring/application/views/scripts/event/show.phtml b/modules/monitoring/application/views/scripts/event/show.phtml new file mode 100644 index 0000000..c844a6f --- /dev/null +++ b/modules/monitoring/application/views/scripts/event/show.phtml @@ -0,0 +1,34 @@ +<?php +use Icinga\Module\Monitoring\Object\Service; + +/** @var string[][] $details */ +/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ +/** @var \Icinga\Web\View $this */ +?> +<div class="controls"> +<?php +if (! $this->compact) { + echo $this->tabs; +} + +echo $object instanceof Service + ? '<h2>' . $this->translate('Current Service State') . '</h2>' . $this->render('partials/object/service-header.phtml') + : '<h2>' . $this->translate('Current Host State') . '</h2>' . $this->render('partials/object/host-header.phtml'); +?> +</div> +<div class="content"> + <?php + foreach ($extensionsHtml as $extensionHtml) { + echo $extensionHtml; + } + ?> + + <h2><?= $this->escape($this->translate('Event Details')) ?></h2> + <table class="event-details name-value-table" data-base-target="_next"> + <?php + foreach ($details as $detail) { + echo '<tr><th>' . $this->escape($detail[0]) . '</th><td>' . $detail[1] . '</td></tr>'; + } + ?> + </table> +</div> diff --git a/modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml b/modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml new file mode 100644 index 0000000..2f81610 --- /dev/null +++ b/modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml @@ -0,0 +1,93 @@ +<?php +/** @var \Icinga\Web\View $this */ +/** @var \Icinga\Module\Monitoring\Forms\Config\TransportReorderForm $form */ +?> +<form id="<?= +$this->escape($form->getId()) +?>" name="<?= +$this->escape($form->getName()) +?>" enctype="<?= +$this->escape($form->getEncType()) +?>" method="<?= +$this->escape($form->getMethod()) +?>" action="<?= +$this->escape($form->getAction()) +?>"> + <table class="table-row-selectable common-table" data-base-target="_next"> + <thead> + <tr> + <th><?= $this->translate('Transport') ?></th> + <th></th> + <th></th> + </tr> + </thead> + <tbody> + <?php + $i = -1; + $transportConfig = $form->getConfig(); + $total = $transportConfig->count(); + foreach ($transportConfig as $transportName => $config): + ++$i; + ?> + <tr> + <td> + <?= $this->qlink( + $transportName, + 'monitoring/config/edittransport', + array('transport' => $transportName), + array( + 'icon' => 'edit', + 'title' => sprintf($this->translate('Edit command transport %s'), $transportName) + ) + ); ?> + <span class="config-label-meta">(<?= sprintf( + $this->translate('Type: %s'), + ucfirst($config->get('transport', 'local')) + ) ?>) + </span> + </td> + <td class="text-right"> + <?= $this->qlink( + '', + 'monitoring/config/removetransport', + array('transport' => $transportName), + array( + 'class' => 'action-link', + 'icon' => 'cancel', + 'title' => sprintf($this->translate('Remove command transport %s'), $transportName) + ) + ); ?> + </td> + <td class="icon-col text-right" data-base-target="_self"> + <?php if ($i > 0): ?> + <button type="submit" name="transport_newpos" class="link-button icon-only animated move-up" value="<?= $this->escape( + ($i - 1) . '|' . $transportName + ) ?>" title="<?= $this->translate( + 'Move up in order' + ) ?>" aria-label="<?= $this->escape(sprintf( + $this->translate('Move command transport %s upwards'), + $transportName + )) ?>"><?= + $this->icon('up-small') + ?></button> + <?php endif ?> + <?php if ($i + 1 < $total): ?> + <button type="submit" name="transport_newpos" class="link-button icon-only animated move-down" value="<?= $this->escape( + ($i + 1) . '|' . $transportName + ) ?>" title="<?= $this->translate( + 'Move down in order' + ) ?>" aria-label="<?= $this->escape(sprintf( + $this->translate('Move command transport %s downwards'), + $transportName + )) ?>"><?= + $this->icon('down-small') + ?></button> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> + <?= $form->getElement($form->getTokenElementName()) ?> + <?= $form->getElement($form->getUidElementName()) ?> +</form> diff --git a/modules/monitoring/application/views/scripts/health/disable-notifications.phtml b/modules/monitoring/application/views/scripts/health/disable-notifications.phtml new file mode 100644 index 0000000..e8c75e5 --- /dev/null +++ b/modules/monitoring/application/views/scripts/health/disable-notifications.phtml @@ -0,0 +1,20 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs->showOnlyCloseButton(); ?> +</div> +<?php endif ?> +<div class="content"> + <h1><?= $title; ?></h1> + <?php if ((bool) $programStatus->notifications_enabled === false): ?> + <div> + <?= $this->translate('Host and service notifications are already disabled.') ?> + <?php if ($this->programStatus->disable_notif_expire_time): ?> + <?= sprintf( + $this->translate('Notifications will be re-enabled in <strong>%s</strong>.'), + $this->timeUntil($this->programStatus->disable_notif_expire_time)); ?> + <?php endif; ?> + </div> + <?php else: ?> + <?= $form; ?> + <?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/health/info.phtml b/modules/monitoring/application/views/scripts/health/info.phtml new file mode 100644 index 0000000..76d9ee3 --- /dev/null +++ b/modules/monitoring/application/views/scripts/health/info.phtml @@ -0,0 +1,87 @@ +<?php +$rv = $this->runtimeVariables()->create($this->runtimevariables); +$cp = $this->checkPerformance()->create($this->checkperformance); + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs; ?> +</div> +<?php endif ?> + +<div class="content processinfo"> + <div class="boxview"> + <div class="box process"> + <h2 tabindex="0"><?= $this->translate('Process Info') ?></h2> + <table class="name-value-table"> + <tbody> + <tr> + <th><?= $this->translate('Program Version') ?></th> + <td><?= $this->programStatus->program_version + ? $this->programStatus->program_version + : $this->translate('N/A') ?></td> + </tr> + <tr> + <th><?= $this->translate('Program Start Time') ?></th> + <td><?= $this->formatDateTime($this->programStatus->program_start_time) ?></td> + </tr> + <tr> + <th><?= $this->translate('Last Status Update'); ?></th> + <td><?= $this->timeAgo($this->programStatus->status_update_time); ?></td> + </tr> + <tr> + <th><?= $this->translate('Last External Command Check'); ?></th> + <td><?= $this->timeAgo($this->programStatus->last_command_check); ?></td> + </tr> + <tr> + <th><?= $this->translate('Last Log File Rotation'); ?></th> + <td><?= $this->programStatus->last_log_rotation + ? $this->timeSince($this->programStatus->last_log_rotation) + : $this->translate('N/A') ?></td> + </tr> + <tr> + <th><?= $this->translate('Global Service Event Handler'); ?></th> + <td><?= $this->programStatus->global_service_event_handler + ? $this->programStatus->global_service_event_handler + : $this->translate('N/A'); ?></td> + </tr> + <tr> + <th><?= $this->translate('Global Host Event Handler'); ?></th> + <td><?= $this->programStatus->global_host_event_handler + ? $this->programStatus->global_host_event_handler + : $this->translate('N/A'); ?></td> + </tr> + <tr> + <th><?= $this->translate('Active Endpoint'); ?></th> + <td><?= $this->programStatus->endpoint_name + ? $this->programStatus->endpoint_name + : $this->translate('N/A') ?></td> + </tr> + <tr> + <th><?= $this->translate('Active Icinga Web 2 Endpoint'); ?></th> + <td><?= gethostname() ?: $this->translate('N/A') ?></td> + </tr> + </tbody> + </table> + <?php if ((bool) $this->programStatus->is_currently_running === true): ?> + <div class="backend-running"> + <?= sprintf( + $this->translate( + '%1$s has been up and running with PID %2$d %3$s', + 'Last format parameter represents the time running' + ), + $this->backendName, + $this->programStatus->process_id, + $this->timeSince($this->programStatus->program_start_time)) ?> + </div> + <?php else: ?> + <div class="backend-not-running"> + <?= sprintf($this->translate('Backend %s is not running'), $this->backendName) ?> + </div> + <?php endif ?> + </div> + <div class="box features"> + <h2 tabindex="0"><?= $this->translate('Feature Commands') ?></h2> + <?= $this->toggleFeaturesForm ?> + </div> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/health/not-running.phtml b/modules/monitoring/application/views/scripts/health/not-running.phtml new file mode 100644 index 0000000..8439fc4 --- /dev/null +++ b/modules/monitoring/application/views/scripts/health/not-running.phtml @@ -0,0 +1,8 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs; ?> +</div> +<?php endif ?> +<div class="content"> + <?= sprintf($this->translate('%s is currently not up and running'), $this->backendName) ?> +</div> diff --git a/modules/monitoring/application/views/scripts/health/stats.phtml b/modules/monitoring/application/views/scripts/health/stats.phtml new file mode 100644 index 0000000..5cfb8f9 --- /dev/null +++ b/modules/monitoring/application/views/scripts/health/stats.phtml @@ -0,0 +1,150 @@ +<?php +$rv = $this->runtimeVariables()->create($this->runtimevariables); +$cp = $this->checkPerformance()->create($this->checkperformance); + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> +</div> +<?php endif ?> + +<div class="content stats"> + <div class="boxview"> + <div class="box stats"> + <h2 tabindex="0"><?= $this->unhandledProblems ?> <?= $this->translate('Unhandled Problems:') ?></h2> + <table class="name-value-table"> + <thead> + <th></th> + <th colspan="3"></th> + </thead> + <tbody> + <tr> + <th><?= $this->translate('Service Problems:') ?></th> + <td colspan="3"> + <span class="badge state-critical"> + <?= + $this->qlink( + $this->unhandledServiceProblems, + 'monitoring/list/services?service_problem=1&service_handled=0&sort=service_severity', + null, + array('data-base-target' => '_next') + ) + ?> + </span> + </td> + </tr> + <tr> + <th><?= $this->translate('Host Problems:') ?></th> + <td colspan="3"> + <span class="badge state-critical"> + <?= + $this->qlink( + $this->unhandledhostProblems, + 'monitoring/list/hosts?host_problem=1&host_handled=0', + null, + array('data-base-target' => '_next') + ) + ?> + </span> + </td> + </tr> + </tbody> + </table> + + <h2 tabindex="0" class="tinystatesummary" data-base-target="_next"> + <?php $this->stats = $hoststats ?> + <?= $this->render('list/components/hostssummary.phtml') ?> + </h2> + <table class="name-value-table"> + <thead> + <tr> + <th><?= $this->translate('Runtime Variables') ?></th> + <th colspan="3"><?= $this->translate('Host Checks') ?></th> + </tr> + </thead> + <tbody> + <tr> + <th><?= $this->translate('Total') ?></th> + <td><?= $rv->total_scheduled_hosts ?></td> + </tr> + <tr> + <th><?= $this->translate('Scheduled') ?></th> + <td><?= $rv->total_scheduled_hosts ?></td> + </tr> + </tbody> + </table> + + <h2 class="tinystatesummary" data-base-target="_next"> + <?php $this->stats = $servicestats ?> + <?= $this->render('list/components/servicesummary.phtml') ?> + </h2> + <table class="name-value-table"> + <thead> + <tr> + <th><?= $this->translate('Runtime Variables') ?></th> + <th><?= $this->translate('Service Checks') ?></th> + <th colspan="2"><?= $this->translate('Per Host') ?></th> + </tr> + </thead> + <tbody> + <tr> + <th><?= $this->translate('Total') ?></th> + <td><?= $rv->total_services ?></td> + <td><?= sprintf('%.2f', $rv->average_services_per_host) ?></td> + </tr> + <tr> + <th><?= $this->translate('Scheduled') ?></th> + <td><?= $rv->total_scheduled_services ?></td> + <td><?= sprintf('%.2f', $rv->average_scheduled_services_per_host) ?></td> + </tr> + </tbody> + </table> + + <h2><?= $this->translate('Active checks') ?></h2> + <table class="name-value-table"> + <thead> + <tr> + <th><?= $this->translate('Check Performance') ?></th> + <th><?= $this->translate('Checks') ?></th> + <th><?= $this->translate('Latency') ?></th> + <th><?= $this->translate('Execution time') ?></th> + </tr> + </thead> + <tbody> + <tr> + <th><?= $this->translate('Host Checks') ?></th> + <td><?= $cp->host_active_count; ?></td> + <td><?= sprintf('%.3f', $cp->host_active_latency_avg) ?>s</td> + <td><?= sprintf('%.3f', $cp->host_active_execution_avg) ?>s</td> + </tr> + <tr> + <th><?= $this->translate('Service Checks') ?></th> + <td><?= $cp->service_active_count; ?></td> + <td><?= sprintf('%.3f', $cp->service_active_latency_avg) ?>s</td> + <td><?= sprintf('%.3f', $cp->service_active_execution_avg) ?>s</td> + </tr> + </tbody> + </table> + + <h2><?= $this->translate('Passive checks') ?></h2> + <table class="name-value-table"> + <thead> + <tr> + <th><?= $this->translate('Check Performance') ?></th> + <th colspan="3"><?= $this->translate('Passive Checks') ?></th> + </tr> + </thead> + <tbody> + <tr> + <th><?= $this->translate('Host Checks') ?></th> + <td><?= $cp->host_passive_count ?></td> + </tr> + <tr> + <th><?= $this->translate('Service Checks') ?></th> + <td><?= $cp->service_passive_count ?></td> + </tr> + </tbody> + </table> + </div> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/host/services.phtml b/modules/monitoring/application/views/scripts/host/services.phtml new file mode 100644 index 0000000..ac1dc5b --- /dev/null +++ b/modules/monitoring/application/views/scripts/host/services.phtml @@ -0,0 +1,23 @@ +<?php use Icinga\Data\Filter\Filter; ?> + +<div class="controls"> + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + <?= $this->render('partials/object/host-header.phtml') ?> + <?php + $this->baseFilter = Filter::where('host', $object->host_name); + $this->stats = $object->stats; + echo $this->render('list/components/servicesummary.phtml'); + ?> +</div> +<?= $this->partial( + 'list/services.phtml', + 'monitoring', + array( + 'compact' => true, + 'showHost' => false, + 'services' => $services, + 'addColumns' => array() + ) +); ?> diff --git a/modules/monitoring/application/views/scripts/host/show.phtml b/modules/monitoring/application/views/scripts/host/show.phtml new file mode 100644 index 0000000..72f5af4 --- /dev/null +++ b/modules/monitoring/application/views/scripts/host/show.phtml @@ -0,0 +1,14 @@ +<?php use Icinga\Data\Filter\Filter; ?> +<div class="controls controls-separated"> +<?php if (! $this->compact): ?> + <?= $this->tabs ?> +<?php endif ?> + <?= $this->render('partials/object/host-header.phtml') ?> +<?php + $this->stats = $object->stats; + $this->baseFilter = Filter::where('host', $object->host_name); + echo $this->render('list/components/servicesummary.phtml'); +?> + <?= $this->render('partials/object/quick-actions.phtml') ?> +</div> +<?= $this->render('partials/object/detail-content.phtml') ?> diff --git a/modules/monitoring/application/views/scripts/hosts/show.phtml b/modules/monitoring/application/views/scripts/hosts/show.phtml new file mode 100644 index 0000000..97b8434 --- /dev/null +++ b/modules/monitoring/application/views/scripts/hosts/show.phtml @@ -0,0 +1,206 @@ +<div class="controls"> + <?php if (! $this->compact): ?> + <?= $tabs; ?> + <?php endif ?> + <?= $this->render('list/components/hostssummary.phtml') ?> + <?= $this->render('partials/host/objects-header.phtml'); ?> + <?php + $hostCount = count($objects); + $unhandledCount = count($unhandledObjects); + $problemCount = count($problemObjects); + $unackCount = count($unacknowledgedObjects); + $scheduledDowntimeCount = count($objects->getScheduledDowntimes()); + ?> +</div> + +<div class="content"> + <?php if ($hostCount === 0): ?> + <?= $this->translate('No hosts found matching the filter'); ?> + <?php else: ?> + <?= $this->render('show/components/extensions.phtml') ?> + <h2><?= $this->translate('Problem Handling') ?></h2> + <table class="name-value-table"> + <tbody> + <?php + + if ($unackCount > 0): ?> + <tr> + <th> <?= sprintf($this->translate('%d unhandled problems'), $unackCount) ?> </th> + <td> + <?= $this->qlink( + $this->translate('Acknowledge'), + $acknowledgeLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'check' + ) + ) ?> + </td> + </tr> + <?php endif; ?> + + <?php if (($acknowledgedCount = count($acknowledgedObjects)) > 0): ?> + <tr> + <th> <?= sprintf( + $this->translatePlural( + '%s acknowledgement', + '%s acknowledgements', + $acknowledgedCount + ), + '<b>' . $acknowledgedCount . '</b>' + ); ?> </th> + <td> + <?= $removeAckForm->setLabelEnabled(true) ?> + </td> + </tr> + <?php endif ?> + + <tr> + <th> <?= $this->translate('Comments') ?> </th> + <td> + <?= $this->qlink( + $this->translate('Add comments'), + $addCommentLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'comment-empty' + ) + ) ?> + </td> + </tr> + + <?php if (($commentCount = count($objects->getComments())) > 0): ?> + <tr> + <th></th> + <td> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%s comment', + '%s comments', + $commentCount + ), + $commentCount + ), + $commentsLink, + null, + array('data-base-target' => '_next') + ); ?> + </td> + </tr> + <?php endif ?> + + <tr> + <th> + <?= $this->translate('Downtimes') ?> + </th> + <td> + <?= $this->qlink( + $this->translate('Schedule downtimes'), + $downtimeAllLink, + null, + array( + 'icon' => 'plug', + 'class' => 'action-link' + ) + ) ?> + </td> + </tr> + + <?php if ($scheduledDowntimeCount > 0): ?> + <tr> + <th></th> + <td> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%d scheduled downtime', + '%d scheduled downtimes', + $scheduledDowntimeCount + ), + $scheduledDowntimeCount + ), + $showDowntimesLink, + null, + array( + 'data-base-target' => '_next' + ) + ) ?> + </td> + </tr> + <?php endif ?> + </tbody> + </table> + + <?php if ($this->hasPermission('monitoring/command/send-custom-notification')): ?> + <h2> <?= $this->translate('Notifications') ?> </h2> + <table class="name-value-table"> + <tbody> + <tr> + <th> <?= $this->translate('Notifications') ?> </th> + <td> + <?= $this->qlink( + $this->translate('Send notifications'), + $sendCustomNotificationLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'bell' + ) + ) ?> + </td> + </tr> + </tbody> + </table> + <?php endif ?> + + <h2> <?= $this->translate('Check Execution') ?> </h2> + + <table class="name-value-table"> + <tbody> + <tr> + <th> <?= $this->translate('Command') ?> </th> + <td> + <?= $this->qlink( + $this->translate('Process check result'), + $processCheckResultAllLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'edit' + ) + ) ?> + </td> + </tr> + + <?php if (isset($checkNowForm)): // Form is unset if the current user lacks the respective permission ?> + <tr> + <th> <?= $this->translate('Schedule Check') ?> </th> + <td> <?= $checkNowForm ?> </td> + </tr> + <?php endif ?> + + <?php if (isset($rescheduleAllLink)): ?> + <tr> + <th></th> + <td> + <?= $this->qlink( + $this->translate('Reschedule'), + $rescheduleAllLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'calendar-empty' + ) + ) ?> + </td> + </tr> + <?php endif ?> + </tbody> + </table> + <h2><?= $this->translate('Feature Commands') ?></h2> + <?= $toggleFeaturesForm ?> + <?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/comments.phtml b/modules/monitoring/application/views/scripts/list/comments.phtml new file mode 100644 index 0000000..c7fb86a --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/comments.phtml @@ -0,0 +1,61 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->render('list/components/selectioninfo.phtml') ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $comments->hasResult()): ?> + <p><?= $this->translate('No comments found matching the filter') ?></p> +</div> +<?php return; endif ?> + <table data-base-target="_next" + class="table-row-selectable common-table multiselect" + data-icinga-multiselect-url="<?= $this->href('monitoring/comments/show') ?>" + data-icinga-multiselect-related="<?= $this->href("monitoring/comments") ?>" + data-icinga-multiselect-data="comment_id"> + <thead class="print-only"> + <tr> + <th><?= $this->translate('Type') ?></th> + <th><?= $this->translate('Comment') ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($comments->peekAhead($this->compact) as $comment): ?> + <tr href="<?= $this->href('monitoring/comment/show', array('comment_id' => $comment->id)) ?>"> + <td class="icon-col"> + <?= $this->partial('partials/comment/comment-description.phtml', array('comment' => $comment)) ?> + </td> + <td> + <?= $this->partial( + 'partials/comment/comment-detail.phtml', + array( + 'comment' => $comment, + 'delCommentForm' => isset($delCommentForm) ? $delCommentForm : null + // Form is unset if the current user lacks the respective permission + )) ?> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($comments->hasMore()): ?> + <div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> + </div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml b/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml new file mode 100644 index 0000000..4b9f1cd --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml @@ -0,0 +1,92 @@ +<?php +use Icinga\Module\Monitoring\Web\Widget\StateBadges; +use Icinga\Web\Url; + +// Don't fetch rows until they are actually needed to improve dashlet performance +if (! $stats instanceof stdClass) { + $stats = $stats->fetchRow(); +} +?> +<div class="hosts-summary dont-print"> + <span class="hosts-link"><?= $this->qlink( + sprintf($this->translatePlural('%u Host', '%u Hosts', $stats->hosts_total), $stats->hosts_total), + // @TODO(el): Fix that + Url::fromPath('monitoring/list/hosts')->setParams(isset($baseFilter) ? $baseFilter->getUrlParams() : array()), + null, + array('title' => sprintf( + $this->translatePlural('List %u host', 'List all %u hosts', $stats->hosts_total), + $stats->hosts_total + )) + ) ?>:</span> +<?php +$stateBadges = new StateBadges(); +$stateBadges + ->setBaseFilter(isset($baseFilter) ? $baseFilter : null) + ->setUrl('monitoring/list/hosts') + ->add( + StateBadges::STATE_UP, + $stats->hosts_up, + array( + 'host_state' => 0 + ), + 'List %u host that is currently in state UP', + 'List %u hosts which are currently in state UP', + array($stats->hosts_up) + ) + ->add( + StateBadges::STATE_DOWN, + $stats->hosts_down_unhandled, + array( + 'host_state' => 1, + 'host_handled' => 0 + ), + 'List %u host that is currently in state DOWN', + 'List %u hosts which are currently in state DOWN', + array($stats->hosts_down_unhandled) + ) + ->add( + StateBadges::STATE_DOWN_HANDLED, + $stats->hosts_down_handled, + array( + 'host_state' => 1, + 'host_handled' => 1 + ), + 'List %u host that is currently in state DOWN (Acknowledged)', + 'List %u hosts which are currently in state DOWN (Acknowledged)', + array($stats->hosts_down_handled) + ) + ->add( + StateBadges::STATE_UNREACHABLE, + $stats->hosts_unreachable_unhandled, + array( + 'host_state' => 2, + 'host_handled' => 0 + ), + 'List %u host that is currently in state UNREACHABLE', + 'List %u hosts which are currently in state UNREACHABLE', + array($stats->hosts_unreachable_unhandled) + ) + ->add( + StateBadges::STATE_UNREACHABLE_HANDLED, + $stats->hosts_unreachable_handled, + array( + 'host_state' => 2, + 'host_handled' => 1 + ), + 'List %u host that is currently in state UNREACHABLE (Acknowledged)', + 'List %u hosts which are currently in state UNREACHABLE (Acknowledged)', + array($stats->hosts_unreachable_handled) + ) + ->add( + StateBadges::STATE_PENDING, + $stats->hosts_pending, + array( + 'host_state' => 99 + ), + 'List %u host that is currently in state PENDING', + 'List %u hosts which are currently in state PENDING', + array($stats->hosts_pending) + ); +echo $stateBadges->render(); +?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml b/modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml new file mode 100644 index 0000000..ec0fb85 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml @@ -0,0 +1,15 @@ +<?php +$helpMessage = $this->translate( + 'Press and hold the Ctrl key while clicking on rows to select multiple rows or press and hold the Shift key to' + .' select a range of rows', + 'Multi-selection help' +); +?> +<div class="selection-info" title="<?= $this->escape($helpMessage) ?>"> + <?= sprintf( + /// TRANSLATORS: Please leave %s as it is because the selection counter is wrapped in a span tag for updating + /// the counter via JavaScript + $this->translate('%s row(s) selected', 'Multi-selection count'), + '<span class="selection-info-count">0</span>' + ) ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml b/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml new file mode 100644 index 0000000..73a3b57 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml @@ -0,0 +1,118 @@ +<?php +use Icinga\Module\Monitoring\Web\Widget\StateBadges; +use Icinga\Web\Url; + +// Don't fetch rows until they are actually needed, to improve dashlet performance +if (! $stats instanceof stdClass) { + $stats = $stats->fetchRow(); +} +?> +<div class="services-summary dont-print"> + <span class="services-link"><?= $this->qlink( + sprintf($this->translatePlural( + '%u Service', '%u Services', $stats->services_total), + $stats->services_total + ), + // @TODO(el): Fix that + Url::fromPath('monitoring/list/services')->setParams(isset($baseFilter) ? $baseFilter->getUrlParams() : array()), + null, + array('title' => sprintf( + $this->translatePlural('List %u service', 'List all %u services', $stats->services_total), + $stats->services_total + )) + ) ?>:</span> +<?php +$stateBadges = new StateBadges(); +$stateBadges + ->setBaseFilter(isset($baseFilter) ? $baseFilter : null) + ->setUrl('monitoring/list/services') + ->add( + StateBadges::STATE_OK, + $stats->services_ok, + array( + 'service_state' => 0 + ), + 'List %u service that is currently in state OK', + 'List %u services which are currently in state OK', + array($stats->services_ok) + ) + ->add( + StateBadges::STATE_CRITICAL, + $stats->services_critical_unhandled, + array( + 'service_state' => 2, + 'service_handled' => 0 + ), + 'List %u service that is currently in state CRITICAL', + 'List %u services which are currently in state CRITICAL', + array($stats->services_critical_unhandled) + ) + ->add( + StateBadges::STATE_CRITICAL_HANDLED, + $stats->services_critical_handled, + array( + 'service_state' => 2, + 'service_handled' => 1 + ), + 'List %u handled service that is currently in state CRITICAL', + 'List %u handled services which are currently in state CRITICAL', + array($stats->services_critical_handled) + ) + ->add( + StateBadges::STATE_UNKNOWN, + $stats->services_unknown_unhandled, + array( + 'service_state' => 3, + 'service_handled' => 0 + ), + 'List %u service that is currently in state UNKNOWN', + 'List %u services which are currently in state UNKNOWN', + array($stats->services_unknown_unhandled) + ) + ->add( + StateBadges::STATE_UNKNOWN_HANDLED, + $stats->services_unknown_handled, + array( + 'service_state' => 3, + 'service_handled' => 1 + ), + 'List %u handled service that is currently in state UNKNOWN', + 'List %u handled services which are currently in state UNKNOWN', + array($stats->services_unknown_handled) + + ) + ->add( + StateBadges::STATE_WARNING, + $stats->services_warning_unhandled, + array( + 'service_state' => 1, + 'service_handled' => 0 + ), + 'List %u service that is currently in state WARNING', + 'List %u services which are currently in state WARNING', + array($stats->services_warning_unhandled) + ) + ->add( + StateBadges::STATE_WARNING_HANDLED, + $stats->services_warning_handled, + array( + 'service_state' => 1, + 'service_handled' => 1 + ), + 'List %u handled service that is currently in state WARNING', + 'List %u handled services which are currently in state WARNING', + array($stats->services_warning_handled) + ) + ->add( + StateBadges::STATE_PENDING, + $stats->services_pending, + array( + 'service_state' => 99 + ), + 'List %u handled service that is currently in state PENDING', + 'List %u handled services which are currently in state PENDING', + array($stats->services_pending) + ); +echo $stateBadges->render(); +?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/contactgroups.phtml b/modules/monitoring/application/views/scripts/list/contactgroups.phtml new file mode 100644 index 0000000..125aeea --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/contactgroups.phtml @@ -0,0 +1,53 @@ +<?php + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $contactGroups->hasResult()): ?> + <p><?= $this->translate('No contact groups found matching the filter') ?></p> +</div> +<?php return; endif ?> + <table class="common-table table-row-selectable" data-base-target="_next"> + <thead> + <tr> + <th></th> + <th><?= $this->translate('Contact Group') ?></th> + <th><?= $this->translate('Alias') ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($contactGroups as $contactGroup): ?> + <tr> + <td class="count-col"> + <span class="badge"><?= $contactGroup->contact_count ?></span> + </td> + <th> + <?= $this->qlink( + $contactGroup->contactgroup_name, + 'monitoring/list/contacts', + array('contactgroup_name' => $contactGroup->contactgroup_name), + array('title' => sprintf( + $this->translate('Show detailed information about %s'), + $contactGroup->contactgroup_name + )) + ) ?> + </th> + <td> + <?php if ($contactGroup->contactgroup_name !== $contactGroup->contactgroup_alias): ?> + <?= $contactGroup->contactgroup_alias ?> + <?php endif ?> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> +</div> diff --git a/modules/monitoring/application/views/scripts/list/contacts.phtml b/modules/monitoring/application/views/scripts/list/contacts.phtml new file mode 100644 index 0000000..42ec778 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/contacts.phtml @@ -0,0 +1,83 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $contacts->hasResult()): ?> + <p><?= $this->translate('No contacts found matching the filter') ?></p> +</div> +<?php return; endif ?> + <table class="common-table table-row-selectable" data-base-target="_next"> + <thead> + <tr> + <th><?= $this->translate('Name') ?></th> + <th><?= $this->translate('Email') ?></th> + <th><?= $this->translate('Pager') ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($contacts->peekAhead($this->compact) as $contact): ?> + <tr> + <th> + <?= $this->qlink( + $contact->contact_name, + 'monitoring/show/contact', + array('contact_name' => $contact->contact_name), + array( + 'title' => sprintf( + $this->translate('Show detailed information about %s'), + $contact->contact_alias + ) + ) + ) ?> + </th> + <td> + <?= $this->translate('Email') ?>: + <a href="mailto:<?= $contact->contact_email ?>" + title="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias) ?>" + aria-label="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias) ?>"> + <?= $this->escape($contact->contact_email) ?> + </a> + </td> + <td> + <?php if ($contact->contact_pager): ?> + <?= $this->escape($contact->contact_pager) ?> + <?php endif ?> + </td> + + <?php if ($contact->contact_notify_service_timeperiod): ?> + <td> + <?= $this->escape($contact->contact_notify_service_timeperiod) ?> + </td> + <?php endif ?> + + <?php if ($contact->contact_notify_host_timeperiod): ?> + <td> + <?= $this->escape($contact->contact_notify_host_timeperiod) ?> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($contacts->hasMore()): ?> + <div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> + </div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/downtimes.phtml b/modules/monitoring/application/views/scripts/list/downtimes.phtml new file mode 100644 index 0000000..46ce0bb --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/downtimes.phtml @@ -0,0 +1,64 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->render('list/components/selectioninfo.phtml') ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $downtimes->hasResult()): ?> + <p><?= $this->translate('No downtimes found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <table class="common-table state-table table-row-selectable multiselect" + data-base-target="_next" + data-icinga-multiselect-url="<?= $this->href('monitoring/downtimes/show') ?>" + data-icinga-multiselect-controllers="<?= $this->href("monitoring/downtimes") ?>" + data-icinga-multiselect-data="downtime_id"> + <thead class="print-only"> + <tr> + <th><?= $this->translate('State') ?></th> + <th><?= $this->translate('Downtime') ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($downtimes->peekAhead($this->compact) as $downtime): + if (isset($downtime->service_description)) { + $this->isService = true; + $this->stateName = Service::getStateText($downtime->service_state); + } else { + $this->isService = false; + $this->stateName = Host::getStateText($downtime->host_state); + } + // Set downtime for partials + $this->downtime = $downtime; + ?> + <tr href="<?= $this->href('monitoring/downtime/show', array('downtime_id' => $downtime->id)) ?>"> + <?= $this->render('partials/downtime/downtime-header.phtml') ?> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($downtimes->hasMore()): ?> + <div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> + </div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/eventgrid.phtml b/modules/monitoring/application/views/scripts/list/eventgrid.phtml new file mode 100644 index 0000000..8809c53 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/eventgrid.phtml @@ -0,0 +1,123 @@ +<?php +use Icinga\Data\Filter\Filter; +use Icinga\Web\Widget\Chart\HistoryColorGrid; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->form ?> +</div> +<?php endif ?> +<div class="content" data-base-target="_next"> +<?php + +$settings = array( + 'cnt_up' => array( + 'tooltip' => $this->translate('%d hosts ok on %s'), + 'color' => '#49DF96', + 'opacity' => '0.55' + ), + 'cnt_unreachable_hard' => array( + 'tooltip' => $this->translate('%d hosts unreachable on %s'), + 'color' => '#77AAFF', + 'opacity' => '0.55' + ), + 'cnt_critical_hard' => array( + 'tooltip' => $this->translate('%d services critical on %s'), + 'color' => '#ff5566', + 'opacity' => '0.9' + ), + + 'cnt_warning_hard' => array( + 'tooltip' => $this->translate('%d services warning on %s'), + 'color' => '#ffaa44', + 'opacity' => '1.0' + ), + + 'cnt_down_hard' => array( + 'tooltip' => $this->translate('%d hosts down on %s'), + 'color' => '#ff5566', + 'opacity' => '0.9' + ), + 'cnt_unknown_hard' => array( + 'tooltip' => $this->translate('%d services unknown on %s'), + 'color' => '#cc77ff', + 'opacity' => '0.7' + ), + 'cnt_ok' => array( + 'tooltip' => $this->translate('%d services ok on %s'), + 'color' => '#49DF96', + 'opacity' => '0.55' + ) +); + +$data = array(); +foreach ($summary as $entry) { + $day = $entry->day; + $value = $entry->$column; + $caption = sprintf( + $settings[$column]['tooltip'], + $value, + $this->formatDate(strtotime($day ?? '')) + ); + $linkFilter = Filter::matchAll( + Filter::expression('timestamp', '<', strtotime($day . ' 23:59:59')), + Filter::expression('timestamp', '>', strtotime($day . ' 00:00:00')), + $form->getFilter(), + $filter + ); + $data[$day] = array( + 'value' => $value, + 'caption' => $caption, + 'url' => $this->href('monitoring/list/eventhistory?' . $linkFilter->toQueryString()) + ); +} + +if (! $summary->hasResult()) { + echo $this->translate('No state changes in the selected time period.') . '</div>'; + return; +} + +$from = intval($form->getValue('from', strtotime('3 months ago'))); +$to = intval($form->getValue('to', time())); + +// don't display more than ten years, or else this will get really slow +if ($to - $from > 315360000) { + $from = $to - 315360000; +} + +$f = new DateTime(); +$f->setTimestamp($from); +$t = new DateTime(); +$t->setTimestamp($to); +$diff = $t->diff($f); +$step = 124; + +for ($i = 0; $i < $diff->days; $i += $step) { + $end = clone $f; + if ($diff->days - $i > $step) { + // full range, move last day to next chunk + $end->add(new DateInterval('P' . ($step - 1) . 'D')); + } else { + // include last day + $end->add(new DateInterval('P' . ($diff->days - $i) . 'D')); + } + $grid = new HistoryColorGrid(null, $f->getTimestamp(), $end->getTimestamp()); + $grid->setColor($settings[$column]['color']); + $grid->opacity = $settings[$column]['opacity']; + $grid->orientation = $orientation; + $grid->setData($data); + $grids[] = $grid; + + $f->add(new DateInterval('P' . $step . 'D')); +} +?> +<div style="width: 33.5em;"> +<?php foreach (array_reverse($grids) as $key => $grid): ?> + <div style=" <?= $this->orientation === 'horizontal' ? '' : 'display: inline-block; vertical-align: top; top; margin: 0.5em;' ?>"> + <?= $grid; ?> + <?= $this->orientation === 'horizontal' ? '<br />' : '' ?> + </div> +<?php endforeach ?> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/list/eventhistory.phtml b/modules/monitoring/application/views/scripts/list/eventhistory.phtml new file mode 100644 index 0000000..0573e8a --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/eventhistory.phtml @@ -0,0 +1,22 @@ +<?php + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<?= $this->partial( + 'partials/event-history.phtml', + array( + 'compact' => $this->compact, + 'history' => $history, + 'isOverview' => true, + 'translationDomain' => $this->translationDomain + ) +) ?> + diff --git a/modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml b/modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml new file mode 100644 index 0000000..34498d0 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml @@ -0,0 +1,173 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <div class="sort-controls-container"> + <?= $this->sortBox ?> + <a href="<?= $this->href('monitoring/list/hostgroups')->addFilter($this->filterEditor->getFilter()) ?>" class="grid-toggle-link" + title="<?= $this->translate('Toogle grid view mode') ?>"> + <?= $this->icon('th-list', null, ['class' => '-inactive']) ?> + <?= $this->icon('th-thumb-empty', null, ['class' => '-active']) ?> + </a> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content" data-base-target="_next"> +<?php /** @var \Icinga\Module\Monitoring\DataView\Hostgroup $hostGroups */ +if (! $hostGroups->hasResult()): ?> + <p><?= $this->translate('No host groups found matching the filter.') ?></p> +</div> +<?php return; endif ?> +<div class="group-grid"> +<?php foreach ($hostGroups as $hostGroup): ?> + <div class="group-grid-cell"> + <?php if ($hostGroup->hosts_down_unhandled > 0): ?> + <?= $this->qlink( + $hostGroup->hosts_down_unhandled, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + [ + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'host_handled' => 0, + 'host_state' => 1 + ], + [ + 'class' => 'state-down', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state DOWN in the host group "%s"', + 'List %u hosts which are currently in state DOWN in the host group "%s"', + $hostGroup->hosts_down_unhandled + ), + $hostGroup->hosts_down_unhandled, + $hostGroup->hostgroup_alias + ) + ] + ) ?> + <?php elseif ($hostGroup->hosts_unreachable_unhandled > 0): ?> + <?= $this->qlink( + $hostGroup->hosts_unreachable_unhandled, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + [ + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'host_handled' => 0, + 'host_state' => 2 + ], + [ + 'class' => 'state-unreachable', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state UNREACHABLE in the host group "%s"', + 'List %u hosts which are currently in state UNREACHABLE in the host group "%s"', + $hostGroup->hosts_unreachable_unhandled + ), + $hostGroup->hosts_unreachable_unhandled, + $hostGroup->hostgroup_alias + ) + ] + ) ?> + <?php elseif ($hostGroup->hosts_down_handled > 0): ?> + <?= $this->qlink( + $hostGroup->hosts_down_handled, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + [ + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'host_handled' => 1, + 'host_state' => 1 + ], + [ + 'class' => 'state-down handled', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state DOWN (Acknowledged) in the host group "%s"', + 'List %u hosts which are currently in state DOWN (Acknowledged) in the host group "%s"', + $hostGroup->hosts_down_handled + ), + $hostGroup->hosts_down_handled, + $hostGroup->hostgroup_alias + ) + ] + ) ?> + <?php elseif ($hostGroup->hosts_unreachable_handled > 0): ?> + <?= $this->qlink( + $hostGroup->hosts_unreachable_handled, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + [ + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'host_handled' => 0, + 'host_state' => 2 + ], + [ + 'class' => 'state-unreachable handled', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state UNREACHABLE (Acknowledged) in the host group "%s"', + 'List %u hosts which are currently in state UNREACHABLE (Acknowledged) in the host group "%s"', + $hostGroup->hosts_unreachable_handled + ), + $hostGroup->hosts_unreachable_handled, + $hostGroup->hostgroup_alias + ) + ] + ) ?> + <?php elseif ($hostGroup->hosts_pending > 0): ?> + <?= $this->qlink( + $hostGroup->hosts_pending, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + [ + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'host_state' => 99 + ], + [ + 'class' => 'state-pending', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state PENDING in the host group "%s"', + 'List %u hosts which are currently in state PENDING in the host group "%s"', + $hostGroup->hosts_pending + ), + $hostGroup->hosts_pending, + $hostGroup->hostgroup_alias + ) + ] + ) ?> + <?php elseif ($hostGroup->hosts_up > 0): ?> + <?= $this->qlink( + $hostGroup->hosts_up, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + [ + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'host_state' => 0 + ], + [ + 'class' => 'state-up', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state UP in the host group "%s"', + 'List %u hosts which are currently in state UP in the host group "%s"', + $hostGroup->hosts_up + ), + $hostGroup->hosts_up, + $hostGroup->hostgroup_alias + ) + ] + ) ?> + <?php else: ?> + <div class="state-none"> + 0 + </div> + <?php endif ?> + <?= $this->qlink( + $hostGroup->hostgroup_alias, + $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()), + ['hostgroup_name' => $hostGroup->hostgroup_name], + [ + 'title' => sprintf( + $this->translate('List all hosts in the group "%s"'), + $hostGroup->hostgroup_alias + ) + ] + ) ?> + </div> +<?php endforeach ?> +</div> +</div> diff --git a/modules/monitoring/application/views/scripts/list/hostgroups.phtml b/modules/monitoring/application/views/scripts/list/hostgroups.phtml new file mode 100644 index 0000000..a0592c8 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/hostgroups.phtml @@ -0,0 +1,296 @@ +<?php + +use Icinga\Module\Monitoring\Web\Widget\StateBadges; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + <a href="<?= $this->href('monitoring/list/hostgroup-grid')->addFilter(clone $this->filterEditor->getFilter()) ?>" class="grid-toggle-link" + title="<?= $this->translate('Toogle grid view mode') ?>"> + <?= $this->icon('th-list', null, ['class' => '-active']) ?> + <?= $this->icon('th-thumb-empty', null, ['class' => '-inactive']) ?> + </a> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> + +<div class="content"> +<?php /** @var \Icinga\Module\Monitoring\DataView\Hostgroup $hostGroups */ if (! $hostGroups->hasResult()): ?> + <p><?= $this->translate('No host groups found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <table class="common-table table-row-selectable" data-base-target="_next"> + <thead> + <tr> + <th></th> + <th><?= $this->translate('Host Group') ?></th> + <th><?= $this->translate('Host States') ?></th> + <th></th> + <th><?= $this->translate('Service States') ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($hostGroups->peekAhead($this->compact) as $hostGroup): ?> + <tr> + <td class="count-col"> + <span class="badge"><?= $hostGroup->hosts_total ?></span> + </td> + <th> + <?= $this->qlink( + $hostGroup->hostgroup_alias, + $this + ->url('monitoring/list/hosts') + ->setParams(['hostgroup_name' => $hostGroup->hostgroup_name]) + ->addFilter($this->filterEditor->getFilter()), + ['sort' => 'host_severity'], + ['title' => sprintf( + $this->translate('List all hosts in the group "%s"'), + $hostGroup->hostgroup_alias + )] + ) ?> + </th> + <td> + <?php + $stateBadges = new StateBadges(); + $stateBadges + ->setUrl('monitoring/list/hosts') + ->setBaseFilter($this->filterEditor->getFilter()) + ->add( + StateBadges::STATE_UP, + $hostGroup->hosts_up, + array( + 'host_state' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'host_severity' + ), + 'List %u host that is currently in state UP in the host group "%s"', + 'List %u hosts which are currently in state UP in the host group "%s"', + array($hostGroup->hosts_up, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_DOWN, + $hostGroup->hosts_down_unhandled, + array( + 'host_state' => 1, + 'host_acknowledged' => 0, + 'host_in_downtime' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'host_severity' + ), + 'List %u host that is currently in state DOWN in the host group "%s"', + 'List %u hosts which are currently in state DOWN in the host group "%s"', + array($hostGroup->hosts_down_unhandled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_DOWN_HANDLED, + $hostGroup->hosts_down_handled, + array( + 'host_state' => 1, + 'host_handled' => 1, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'host_severity' + ), + 'List %u host that is currently in state DOWN (Acknowledged) in the host group "%s"', + 'List %u hosts which are currently in state DOWN (Acknowledged) in the host group "%s"', + array($hostGroup->hosts_down_handled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_UNREACHABLE, + $hostGroup->hosts_unreachable_unhandled, + array( + 'host_state' => 2, + 'host_acknowledged' => 0, + 'host_in_downtime' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'host_severity' + ), + 'List %u host that is currently in state UNREACHABLE in the host group "%s"', + 'List %u hosts which are currently in state UNREACHABLE in the host group "%s"', + array($hostGroup->hosts_unreachable_unhandled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_UNREACHABLE_HANDLED, + $hostGroup->hosts_unreachable_handled, + array( + 'host_state' => 2, + 'host_handled' => 1, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'host_severity' + ), + 'List %u host that is currently in state UNREACHABLE (Acknowledged) in the host group "%s"', + 'List %u hosts which are currently in state UNREACHABLE (Acknowledged) in the host group "%s"', + array($hostGroup->hosts_unreachable_handled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_PENDING, + $hostGroup->hosts_pending, + array( + 'host_state' => 99, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'host_severity' + ), + 'List %u host that is currently in state PENDING in the host group "%s"', + 'List %u hosts which are currently in state PENDING in the host group "%s"', + array($hostGroup->hosts_pending, $hostGroup->hostgroup_alias) + ); + echo $stateBadges->render(); + ?> + </td> + <td class="count-col"> + <?= $this->qlink( + $hostGroup->services_total, + $this + ->url('monitoring/list/services') + ->setParams(['hostgroup_name' => $hostGroup->hostgroup_name]) + ->addFilter($this->filterEditor->getFilter()), + ['sort' => 'service_severity'], + [ + 'title' => sprintf( + $this->translate('List all services of all hosts in host group "%s"'), + $hostGroup->hostgroup_alias + ), + 'class' => 'badge' + ] + ) ?> + </td> + <td> + <?php + $stateBadges = new StateBadges(); + $stateBadges + ->setUrl('monitoring/list/services') + ->setBaseFilter($this->filterEditor->getFilter()) + ->add( + StateBadges::STATE_OK, + $hostGroup->services_ok, + array( + 'service_state' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state OK on hosts in the host group "%s"', + 'List %u services which are currently in state OK on hosts in the host group "%s"', + array($hostGroup->services_ok, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_CRITICAL, + $hostGroup->services_critical_unhandled, + array( + 'service_state' => 2, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state CRITICAL on hosts in the host group "%s"', + 'List %u services which are currently in state CRITICAL on hosts in the host group "%s"', + array($hostGroup->services_critical_unhandled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_CRITICAL_HANDLED, + $hostGroup->services_critical_handled, + array( + 'service_state' => 2, + 'service_handled' => 1, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state CRITICAL (Acknowledged) on hosts in the host group "%s"', + 'List %u services which are currently in state CRITICAL (Acknowledged) on hosts in the host group "%s"', + array($hostGroup->services_critical_handled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_UNKNOWN, + $hostGroup->services_unknown_unhandled, + array( + 'service_state' => 3, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state UNKNOWN on hosts in the host group "%s"', + 'List %u services which are currently in state UNKNOWN on hosts in the host group "%s"', + array($hostGroup->services_unknown_unhandled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_UNKNOWN_HANDLED, + $hostGroup->services_unknown_handled, + array( + 'service_state' => 3, + 'service_handled' => 1, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state UNKNOWN (Acknowledged) on hosts in the host group "%s"', + 'List %u services which are currently in state UNKNOWN (Acknowledged) on hosts in the host group "%s"', + array($hostGroup->services_unknown_handled, $hostGroup->hostgroup_alias) + + ) + ->add( + StateBadges::STATE_WARNING, + $hostGroup->services_warning_unhandled, + array( + 'service_state' => 1, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state WARNING on hosts in the host group "%s"', + 'List %u services which are currently in state WARNING on hosts in the host group "%s"', + array($hostGroup->services_warning_unhandled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_WARNING_HANDLED, + $hostGroup->services_warning_handled, + array( + 'service_state' => 1, + 'service_handled' => 1, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state WARNING (Acknowledged) on hosts in the host group "%s"', + 'List %u services which are currently in state WARNING (Acknowledged) on hosts in the host group "%s"', + array($hostGroup->services_warning_handled, $hostGroup->hostgroup_alias) + ) + ->add( + StateBadges::STATE_PENDING, + $hostGroup->services_pending, + array( + 'service_state' => 99, + 'hostgroup_name' => $hostGroup->hostgroup_name, + 'sort' => 'service_severity' + ), + 'List %u service that is currently in state PENDING on hosts in the host group "%s"', + 'List %u services which are currently in state PENDING on hosts in the host group "%s"', + array($hostGroup->services_pending, $hostGroup->hostgroup_alias) + ); + echo $stateBadges->render(); + ?> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($hostGroups->hasMore()): ?> + <div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> + </div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml new file mode 100644 index 0000000..6d7674e --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/hosts.phtml @@ -0,0 +1,106 @@ +<?php +use Icinga\Date\DateFormatter; +use Icinga\Module\Monitoring\Object\Host; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $hosts->hasResult()): ?> + <p><?= $this->translate('No hosts found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <table data-base-target="_next" + class="table-row-selectable state-table multiselect" + data-icinga-multiselect-url="<?= $this->href('monitoring/hosts/show') ?>" + data-icinga-multiselect-controllers="<?= $this->href("monitoring/hosts") ?>" + data-icinga-multiselect-data="host"> + <thead class="print-only"> + <tr> + <th><?= $this->translate('State') ?></th> + <th><?= $this->translate('Host') ?></th> + <?php foreach($this->addColumns as $col): ?> + <th><?= $this->escape($col) ?></th> + <?php endforeach ?> + </tr> + </thead> + <tbody> + <?php foreach($hosts->peekAhead($this->compact) as $host): + $hostStateName = Host::getStateText($host->host_state); + $hostLink = $this->href('monitoring/host/show', array('host' => $host->host_name)); + $hostCheckOverdue = $host->host_next_update < time();?> + <tr<?= $hostCheckOverdue ? ' class="state-outdated"' : '' ?>> + <td class="state-col state-<?= $hostStateName ?><?= $host->host_handled ? ' handled' : '' ?>"> + <div class="state-label"> + <?php if ($hostCheckOverdue): ?> + <?= $this->icon('clock', sprintf($this->translate('Overdue %s'), DateFormatter::timeSince($host->host_next_update))) ?> + <?php endif ?> + <?= Host::getStateText($host->host_state, true) ?> + </div> + <?php if ((int) $host->host_state !== 99): ?> + <div class="state-meta"> + <?= $this->timeSince($host->host_last_state_change, $this->compact) ?> + <?php if ((int) $host->host_state > 0 && (int) $host->host_state_type === 0): ?> + <div><?= $this->translate('Soft', 'Soft state') ?> <?= $host->host_attempt ?></div> + <?php endif ?> + </div> + <?php endif ?> + </td> + <td> + <div class="state-header"> + <?= $this->iconImage()->host($host) ?> + <?= $this->qlink( + $host->host_display_name, + $hostLink, + null, + array( + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $host->host_display_name + ), + 'class' => 'rowaction' + ) + ) ?> + <span class="state-icons"><?= $this->hostFlags($host) ?></span> + </div> + <p class="overview-plugin-output"><?= $this->pluginOutput($this->ellipsis($host->host_output, 10000), true, $host->host_check_command) ?></p> + </td> + <?php foreach($this->addColumns as $col): ?> + <?php if ($host->$col && preg_match('~^_(host|service)_([a-zA-Z0-9_]+)$~', $col, $m)): ?> + <td><?= $this->escape(\Icinga\Module\Monitoring\Object\MonitoredObject::protectCustomVars([$m[2] => $host->$col])[$m[2]]) ?></td> + <?php else: ?> + <td><?= $this->escape($host->$col) ?></td> + <?php endif ?> + <?php endforeach ?> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($hosts->hasMore()): ?> + <div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> + </div> +<?php endif ?> +</div> +<?php if (! $this->compact): ?> +<div class="monitoring-statusbar dont-print"> + <?= $this->render('list/components/hostssummary.phtml') ?> + <?= $this->render('list/components/selectioninfo.phtml') ?> +</div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/list/notifications.phtml b/modules/monitoring/application/views/scripts/list/notifications.phtml new file mode 100644 index 0000000..51ef432 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/notifications.phtml @@ -0,0 +1,124 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $notifications->hasResult()): ?> + <p><?= $this->translate('No notifications found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <table data-base-target="_next" class="table-row-selectable state-table"> + <tbody> + <?php foreach ($notifications->peekAhead($this->compact) as $notification): + if (isset($notification->service_description)) { + $isService = true; + $stateLabel = Service::getStateText($notification->notification_state, true); + $stateName = Service::getStateText($notification->notification_state); + } else { + $isService = false; + $stateLabel = Host::getStateText($notification->notification_state, true); + $stateName = Host::getStateText($notification->notification_state); + } + ?> + <tr href="<?= $this->href('monitoring/event/show', ['id' => $notification->id, 'type' => 'notify']) ?>"> + <td class="state-col state-<?= $stateName ?>"> + <div class="state-label"><?= $stateLabel ?></div> + <div class="state-meta"> + <?= $this->formatDateTime($notification->notification_timestamp) ?> + </div> + </td> + <td> + <div class="state-header"> + <?php if ($isService) { + echo '<span class="service-on">'; + echo sprintf( + $this->translate('%s on %s', 'service on host'), + $this->qlink( + $notification->service_display_name, + 'monitoring/service/show', + [ + 'host' => $notification->host_name, + 'service' => $notification->service_description + ], + [ + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $notification->service_display_name, + $notification->host_display_name + ) + ] + ), + $this->qlink( + $notification->host_display_name, + 'monitoring/host/show', + ['host' => $notification->host_name], + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $notification->host_display_name + ) + ] + ) + ); + echo '</span>'; + } else { + echo $this->qlink( + $notification->host_display_name, + 'monitoring/host/show', + ['host' => $notification->host_name], + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $notification->host_display_name + ) + ] + ); + } ?> + <?php if (! $this->contact): ?> + <div class="notification-recipient"> + <?php if ($notification->notification_contact_name): ?> + <?= sprintf( + $this->translate('Sent to %s'), + $this->qlink( + $notification->notification_contact_name, + 'monitoring/show/contact', + array('contact_name' => $notification->notification_contact_name) + ) + ) ?> + <?php else: ?> + <?= $this->translate('Not sent out to any contact') ?> + <?php endif ?> + </div> + <?php endif ?> + </div> + <p class="overview-plugin-output"><?= $this->pluginOutput($this->ellipsis($notification->notification_output, 10000), true) ?></p> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($notifications->hasMore()): ?> + <div class="action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url(isset($notificationsUrl) ? $notificationsUrl : null)->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ); ?> + </div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml b/modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml new file mode 100644 index 0000000..d7b4c78 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml @@ -0,0 +1,144 @@ +<?php +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Url; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->problemToggle ?> + <div class="sort-controls-container"> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content" data-base-target="_next"> + <?php if (empty($pivotData)): ?> + <p><?= $this->translate('No services found matching the filter.') ?></p> +</div> +<?php return; endif; +$serviceFilter = Filter::matchAny(); +foreach ($pivotData as $serviceDescription => $_) { + $serviceFilter->orFilter(Filter::where('service_description', $serviceDescription)); +} +?> +<table class="service-grid-table"> + <thead> + <tr> + <th><?= $this->partial( + 'joystickPagination.phtml', + 'default', + array( + 'flippable' => true, + 'xAxisPaginator' => $horizontalPaginator, + 'yAxisPaginator' => $verticalPaginator + ) + ) ?></th> + <?php foreach ($pivotHeader['cols'] as $hostName => $hostAlias): ?> + <th class="rotate-45"><div><span><?= $this->qlink( + $this->ellipsis($hostAlias, 24), + Url::fromPath('monitoring/list/services')->addFilter( + Filter::matchAll($serviceFilter, Filter::where('host_name', $hostName)) + ), + null, + array('title' => sprintf($this->translate('List all reported services on host %s'), $hostAlias)), + false + ) ?></span></div></th> + <?php endforeach ?> + </tr> + </thead> + <tbody> + + <?php $i = 0 ?> + <?php foreach ($pivotHeader['rows'] as $serviceDescription => $serviceDisplayName): ?> + <tr> + <th><?php + $hostFilter = Filter::matchAny(); + foreach ($pivotData[$serviceDescription] as $hostName => $_) { + $hostFilter->orFilter(Filter::where('host_name', $hostName)); + } + echo $this->qlink( + $serviceDisplayName, + Url::fromPath('monitoring/list/services')->addFilter( + Filter::matchAll($hostFilter, Filter::where('service_description', $serviceDescription)) + ), + null, + array('title' => sprintf( + $this->translate('List all services with the name "%s" on all reported hosts'), + $serviceDisplayName + )) + ); + ?></th> + <?php foreach (array_keys($pivotHeader['cols']) as $hostName): ?> + <td><?php + $service = $pivotData[$serviceDescription][$hostName]; + if ($service === null): ?> + <span aria-hidden="true">·</span> + <?php continue; endif ?> + <?php $ariaDescribedById = $this->protectId($service->host_name . '_' . $service->service_description . '_desc') ?> + <span class="sr-only" id="<?= $ariaDescribedById ?>"> + <?= $this->escape($service->service_output) ?> + </span> + <?= $this->qlink( + '', + 'monitoring/service/show', + array( + 'host' => $hostName, + 'service' => $serviceDescription + ), + array( + 'aria-describedby' => $ariaDescribedById, + 'aria-label' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $service->service_display_name, + $service->host_display_name + ), + 'class' => 'service-grid-link state-' . Service::getStateText($service->service_state) . ($service->service_handled ? ' handled' : ''), + 'title' => $service->service_output + ) + ) ?> + </td> + <?php endforeach ?> + <?php if (! $this->compact && $this->horizontalPaginator->getPages()->pageCount > 1): ?> + <td> + <?php $expandLink = $this->qlink( + $this->translate('Load more'), + Url::fromRequest(), + array( + 'limit' => ($this->horizontalPaginator->getItemCountPerPage() + 20) + . ',' + . $this->verticalPaginator->getItemCountPerPage() + ), + array( + 'class' => 'action-link', + 'data-base-target' => '_self' + ) + ) ?> + <?= ++$i === (int) ceil(count($pivotHeader['rows']) / 2) ? $expandLink : '' ?> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + <?php if (! $this->compact && $this->verticalPaginator->getPages()->pageCount > 1): ?> + <tr> + <td colspan="<?= count($pivotHeader['cols']) + 1?>" class="service-grid-table-more"> + <?php echo $this->qlink( + $this->translate('Load more'), + Url::fromRequest(), + array( + 'limit' => $this->horizontalPaginator->getItemCountPerPage() + . ',' + . ($this->verticalPaginator->getItemCountPerPage() + 20) + ), + array( + 'class' => 'action-link', + 'data-base-target' => '_self' + ) + ) ?> + </td> + </tr> + <?php endif ?> + </tbody> +</table> +</div> diff --git a/modules/monitoring/application/views/scripts/list/servicegrid.phtml b/modules/monitoring/application/views/scripts/list/servicegrid.phtml new file mode 100644 index 0000000..d0ed4bc --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/servicegrid.phtml @@ -0,0 +1,144 @@ +<?php +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Url; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->problemToggle ?> + <div class="sort-controls-container"> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content" data-base-target="_next"> +<?php if (empty($pivotData)): ?> + <p><?= $this->translate('No services found matching the filter.') ?></p> +</div> +<?php return; endif; +$hostFilter = Filter::matchAny(); +foreach ($pivotData as $hostName => $_) { + $hostFilter->orFilter(Filter::where('host_name', $hostName)); +} +?> + <table class="service-grid-table"> + <thead> + <tr> + <th><?= $this->partial( + 'joystickPagination.phtml', + 'default', + array( + 'flippable' => true, + 'xAxisPaginator' => $horizontalPaginator, + 'yAxisPaginator' => $verticalPaginator + ) + ) ?></th> + <?php foreach ($pivotHeader['cols'] as $serviceDescription => $serviceDisplayName): ?> + <th class="rotate-45"><div><span><?= $this->qlink( + $this->ellipsis($serviceDisplayName, 24), + Url::fromPath('monitoring/list/services')->addFilter( + Filter::matchAll($hostFilter, Filter::where('service_description', $serviceDescription)) + ), + null, + array('title' => sprintf( + $this->translate('List all services with the name "%s" on all reported hosts'), + $serviceDisplayName + )), + false + ) ?></span></div></th> + <?php endforeach ?> + </tr> + </thead> + <tbody> + + <?php $i = 0 ?> + <?php foreach ($pivotHeader['rows'] as $hostName => $hostDisplayName): ?> + <tr> + <th><?php + $serviceFilter = Filter::matchAny(); + foreach ($pivotData[$hostName] as $serviceName => $_) { + $serviceFilter->orFilter(Filter::where('service_description', $serviceName)); + } + echo $this->qlink( + $hostDisplayName, + Url::fromPath('monitoring/list/services')->addFilter( + Filter::matchAll($serviceFilter, Filter::where('host_name', $hostName)) + ), + null, + array('title' => sprintf($this->translate('List all reported services on host %s'), $hostDisplayName)) + ); + ?></th> + <?php foreach (array_keys($pivotHeader['cols']) as $serviceDescription): ?> + <td> + <?php + $service = $pivotData[$hostName][$serviceDescription]; + if ($service === null): ?> + <span aria-hidden="true">·</span> + <?php continue; endif ?> + <?php $ariaDescribedById = $this->protectId($service->host_name . '_' . $service->service_description . '_desc') ?> + <span class="sr-only" id="<?= $ariaDescribedById ?>"> + <?= $this->escape($service->service_output) ?> + </span> + <?= $this->qlink( + '', + 'monitoring/service/show', + array( + 'host' => $hostName, + 'service' => $serviceDescription + ), + array( + 'aria-describedby' => $ariaDescribedById, + 'aria-label' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $service->service_display_name, + $service->host_display_name + ), + 'class' => 'service-grid-link state-' . Service::getStateText($service->service_state) . ($service->service_handled ? ' handled' : ''), + 'title' => $service->service_output + ) + ) ?> + </td> + <?php endforeach ?> + <?php if (! $this->compact && $this->horizontalPaginator->getPages()->pageCount > 1): ?> + <td> + <?php $expandLink = $this->qlink( + $this->translate('Load more'), + Url::fromRequest(), + array( + 'limit' => ( + $this->horizontalPaginator->getItemCountPerPage() + 20) . ',' + . $this->verticalPaginator->getItemCountPerPage() + ), + array( + 'class' => 'action-link', + 'data-base-target' => '_self' + ) + ) ?> + <?= ++$i === (int) (count($pivotHeader['rows']) / 2) ? $expandLink : '' ?> + </td> + <?php endif ?> + </tr> + <?php endforeach ?> + <?php if (! $this->compact && $this->verticalPaginator->getPages()->pageCount > 1): ?> + <tr> + <td colspan="<?= count($pivotHeader['cols']) + 1?>" class="service-grid-table-more"> + <?php echo $this->qlink( + $this->translate('Load more'), + Url::fromRequest(), + array( + 'limit' => $this->horizontalPaginator->getItemCountPerPage() . ',' . + ($this->verticalPaginator->getItemCountPerPage() + 20) + ), + array( + 'class' => 'action-link', + 'data-base-target' => '_self' + ) + ) ?> + </td> + </tr> + <?php endif ?> + </tbody> + </table> +</div> diff --git a/modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml b/modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml new file mode 100644 index 0000000..5ea6d17 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml @@ -0,0 +1,217 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <div class="sort-controls-container"> + <?= $this->sortBox ?> + <a href="<?= $this->href('monitoring/list/servicegroups')->addFilter($this->filterEditor->getFilter()) ?>" class="grid-toggle-link" + title="<?= $this->translate('Toogle grid view mode') ?>"> + <?= $this->icon('th-list', null, ['class' => '-inactive']) ?> + <?= $this->icon('th-thumb-empty', null, ['class' => '-active']) ?> + </a> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content" data-base-target="_next"> +<?php /** @var \Icinga\Module\Monitoring\DataView\Servicegroup $serviceGroups */ +if (! $serviceGroups->hasResult()): ?> + <p><?= $this->translate('No service groups found matching the filter.') ?></p> +</div> +<?php return; endif ?> +<div class="group-grid"> +<?php foreach ($serviceGroups as $serviceGroup): ?> + <div class="group-grid-cell"> + <?php if ($serviceGroup->services_critical_unhandled > 0): ?> + <?= $this->qlink( + $serviceGroup->services_critical_unhandled, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_handled' => 0, + 'service_state' => 2 + ], + [ + 'class' => 'state-critical', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state CRITICAL in service group "%s"', + 'List %s services which are currently in state CRITICAL in service group "%s"', + $serviceGroup->services_critical_unhandled + ), + $serviceGroup->services_critical_unhandled, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_warning_unhandled > 0): ?> + <?= $this->qlink( + $serviceGroup->services_warning_unhandled, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_handled' => 0, + 'service_state' => 1 + ], + [ + 'class' => 'state-warning', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state WARNING in service group "%s"', + 'List %s services which are currently in state WARNING in service group "%s"', + $serviceGroup->services_warning_unhandled + ), + $serviceGroup->services_warning_unhandled, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_unknown_unhandled > 0): ?> + <?= $this->qlink( + $serviceGroup->services_unknown_unhandled, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_handled' => 0, + 'service_state' => 3 + ], + [ + 'class' => 'state-unknown', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state UNKNOWN in service group "%s"', + 'List %s services which are currently in state UNKNOWN in service group "%s"', + $serviceGroup->services_unknown_unhandled + ), + $serviceGroup->services_unknown_unhandled, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_critical_handled > 0): ?> + <?= $this->qlink( + $serviceGroup->services_critical_handled, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_handled' => 1, + 'service_state' => 2 + ], + [ + 'class' => 'state-critical handled', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state CRITICAL (Acknowledged) in service group "%s"', + 'List %s services which are currently in state CRITICAL (Acknowledged) in service group "%s"', + $serviceGroup->services_critical_handled + ), + $serviceGroup->services_critical_handled, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_warning_handled > 0): ?> + <?= $this->qlink( + $serviceGroup->services_warning_handled, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_handled' => 1, + 'service_state' => 1 + ], + [ + 'class' => 'state-warning handled', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state WARNING (Acknowledged) in service group "%s"', + 'List %s services which are currently in state WARNING (Acknowledged) in service group "%s"', + $serviceGroup->services_warning_handled + ), + $serviceGroup->services_warning_handled, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_unknown_handled > 0): ?> + <?= $this->qlink( + $serviceGroup->services_unknown_handled, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_handled' => 1, + 'service_state' => 3 + ], + [ + 'class' => 'state-unknown handled', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state UNKNOWN (Acknowledged) in service group "%s"', + 'List %s services which are currently in state UNKNOWN (Acknowledged) in service group "%s"', + $serviceGroup->services_unknown_handled + ), + $serviceGroup->services_unknown_handled, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_pending > 0): ?> + <?= $this->qlink( + $serviceGroup->services_pending, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_state' => 99 + ], + [ + 'class' => 'state-pending', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currenlty in state PENDING in service group "%s"', + 'List %s services which are currently in state PENDING in service group "%s"', + $serviceGroup->services_pending + ), + $serviceGroup->services_pending, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php elseif ($serviceGroup->services_ok > 0): ?> + <?= $this->qlink( + $serviceGroup->services_ok, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + [ + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'service_state' => 0 + ], + [ + 'class' => 'state-ok', + 'title' => sprintf( + $this->translatePlural( + 'List %s service that is currently in state OK in service group "%s"', + 'List %s services which are currently in state OK in service group "%s"', + $serviceGroup->services_ok + ), + $serviceGroup->services_ok, + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + <?php else: ?> + <div class="state-none"> + 0 + </div> + <?php endif ?> + <?= $this->qlink( + $serviceGroup->servicegroup_alias, + $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()), + ['servicegroup_name' => $serviceGroup->servicegroup_name], + [ + 'title' => sprintf( + $this->translate('List all services in the group "%s"'), + $serviceGroup->servicegroup_alias + ) + ] + ) ?> + </div> +<?php endforeach ?> +</div> +</div> diff --git a/modules/monitoring/application/views/scripts/list/servicegroups.phtml b/modules/monitoring/application/views/scripts/list/servicegroups.phtml new file mode 100644 index 0000000..c915b30 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/servicegroups.phtml @@ -0,0 +1,184 @@ +<?php use Icinga\Module\Monitoring\Web\Widget\StateBadges; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + <a href="<?= $this->href('monitoring/list/servicegroup-grid')->addFilter(clone $this->filterEditor->getFilter()) ?>" class="grid-toggle-link" + title="<?= $this->translate('Toogle grid view mode') ?>"> + <?= $this->icon('th-list', null, ['class' => '-active']) ?> + <?= $this->icon('th-thumb-empty', null, ['class' => '-inactive']) ?> + </a> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $serviceGroups->hasResult()): ?> + <p><?= $this->translate('No service groups found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <table class="table-row-selectable common-table" data-base-target="_next"> + <thead> + <tr> + <th></th> + <th><?= $this->translate('Service Group') ?></th> + <th><?= $this->translate('Service States') ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($serviceGroups->peekAhead($this->compact) as $serviceGroup): ?> + <tr> + <td class="count-col"> + <span class="badge"><?= $serviceGroup->services_total ?></span> + </td> + <th> + <?= $this->qlink( + $serviceGroup->servicegroup_alias, + $this + ->url('monitoring/list/services') + ->setParams(['servicegroup_name' => $serviceGroup->servicegroup_name]) + ->addFilter($this->filterEditor->getFilter()), + ['sort' => 'service_severity'], + ['title' => sprintf($this->translate('List all services in the group "%s"'), $serviceGroup->servicegroup_alias)] + ) ?> + </th> + <td> + <?php + $stateBadges = new StateBadges(); + $stateBadges + ->setUrl('monitoring/list/services') + ->setBaseFilter($this->filterEditor->getFilter()) + ->add( + StateBadges::STATE_OK, + $serviceGroup->services_ok, + array( + 'service_state' => 0, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state OK in service group "%s"', + 'List %s services which are currently in state OK in service group "%s"', + array($serviceGroup->services_ok, $serviceGroup->servicegroup_alias) + ) + ->add( + StateBadges::STATE_CRITICAL, + $serviceGroup->services_critical_unhandled, + array( + 'service_state' => 2, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state CRITICAL in service group "%s"', + 'List %s services which are currently in state CRITICAL in service group "%s"', + array($serviceGroup->services_critical_unhandled, $serviceGroup->servicegroup_alias) + ) + ->add( + StateBadges::STATE_CRITICAL_HANDLED, + $serviceGroup->services_critical_handled, + array( + 'service_state' => 2, + 'service_handled' => 1, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state CRITICAL (Acknowledged) in service group "%s"', + 'List %s services which are currently in state CRITICAL (Acknowledged) in service group "%s"', + array($serviceGroup->services_critical_handled, $serviceGroup->servicegroup_alias) + ) + ->add( + StateBadges::STATE_UNKNOWN, + $serviceGroup->services_unknown_unhandled, + array( + 'service_state' => 3, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state UNKNOWN in service group "%s"', + 'List %s services which are currently in state UNKNOWN in service group "%s"', + array($serviceGroup->services_unknown_unhandled, $serviceGroup->servicegroup_alias) + ) + ->add( + StateBadges::STATE_UNKNOWN_HANDLED, + $serviceGroup->services_unknown_handled, + array( + 'service_state' => 3, + 'service_handled' => 1, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state UNKNOWN (Acknowledged) in service group "%s"', + 'List %s services which are currently in state UNKNOWN (Acknowledged) in service group "%s"', + array($serviceGroup->services_unknown_handled, $serviceGroup->servicegroup_alias) + + ) + ->add( + StateBadges::STATE_WARNING, + $serviceGroup->services_warning_unhandled, + array( + 'service_state' => 1, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state WARNING in service group "%s"', + 'List %s services which are currently in state WARNING in service group "%s"', + array($serviceGroup->services_warning_unhandled, $serviceGroup->servicegroup_alias) + ) + ->add( + StateBadges::STATE_WARNING_HANDLED, + $serviceGroup->services_warning_handled, + array( + 'service_state' => 1, + 'service_handled' => 1, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currently in state WARNING (Acknowledged) in service group "%s"', + 'List %s services which are currently in state WARNING (Acknowledged) in service group "%s"', + array($serviceGroup->services_warning_handled, $serviceGroup->servicegroup_alias) + ) + ->add( + StateBadges::STATE_PENDING, + $serviceGroup->services_pending, + array( + 'service_state' => 99, + 'servicegroup_name' => $serviceGroup->servicegroup_name, + 'sort' => 'service_severity' + ), + 'List %s service that is currenlty in state PENDING in service group "%s"', + 'List %s services which are currently in state PENDING in service group "%s"', + array($serviceGroup->services_pending, $serviceGroup->servicegroup_alias) + ); + echo $stateBadges->render(); + ?> + </td> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($serviceGroups->hasMore()): ?> +<div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> +</div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/list/services.phtml b/modules/monitoring/application/views/scripts/list/services.phtml new file mode 100644 index 0000000..b2088e9 --- /dev/null +++ b/modules/monitoring/application/views/scripts/list/services.phtml @@ -0,0 +1,161 @@ +<?php +use Icinga\Date\DateFormatter; +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content"> +<?php if (! $services->hasResult()): ?> + <p><?= $this->translate('No services found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <table data-base-target="_next" + class="table-row-selectable state-table multiselect<?php if ($this->compact): ?> compact<?php endif ?>" + data-icinga-multiselect-url="<?= $this->href('monitoring/services/show') ?>" + data-icinga-multiselect-controllers="<?= $this->href('monitoring/services') ?>" + data-icinga-multiselect-data="service,host"> + <thead class="print-only"> + <tr> + <th><?= $this->translate('State') ?></th> + <th><?= $this->translate('Service') ?></th> + <?php foreach($this->addColumns as $col): ?> + <th><?= $this->escape($col) ?></th> + <?php endforeach ?> + </tr> + </thead> + <tbody> + <?php foreach ($services->peekAhead($this->compact) as $service): + $serviceLink = $this->href( + 'monitoring/service/show', + array( + 'host' => $service->host_name, + 'service' => $service->service_description + ) + ); + $hostLink = $this->href( + 'monitoring/host/show', + array( + 'host' => $service->host_name, + ) + ); + $serviceStateName = Service::getStateText($service->service_state); + $serviceCheckOverdue = $service->service_next_update < time(); ?> + <tr<?= $serviceCheckOverdue ? ' class="state-outdated"' : '' ?>> + <td class="state-col state-<?= $serviceStateName ?><?= $service->service_handled ? ' handled' : '' ?>"> + <div class="state-label"> + <?php if ($serviceCheckOverdue): ?> + <?= $this->icon('clock', sprintf($this->translate('Overdue %s'), DateFormatter::timeSince($service->service_next_update))) ?> + <?php endif ?> + <?= Service::getStateText($service->service_state, true) ?> + </div> + <?php if ((int) $service->service_state !== 99): ?> + <div class="state-meta"> + <?= $this->timeSince($service->service_last_state_change, $this->compact) ?> + <?php if ((int) $service->service_state > 0 && (int) $service->service_state_type === 0): ?> + <div><?= $this->translate('Soft', 'Soft state') ?> <?= $service->service_attempt ?></div> + <?php endif ?> + </div> + <?php endif ?> + </td> + + <td> + <div class="state-header"> + <span class="service-on"> + <?= $this->iconImage()->service($service) ?> + <?php + if ($this->showHost) { + echo sprintf( + $this->translate('%s on %s', 'service on host'), + $this->qlink( + $service->service_display_name, + $serviceLink, + null, + array( + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $service->service_display_name, + $service->host_display_name + ), + 'class' => 'rowaction' + ) + ), + $this->qlink( + $service->host_display_name + . ($service->host_state != 0 ? ' (' . Host::getStateText($service->host_state, true) . ')' : ''), + $hostLink, + null, + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $service->host_display_name + ) + ] + ) + ); + } else { + echo $this->qlink( + $service->service_display_name, + $serviceLink, + null, + array( + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $service->service_display_name, + $service->host_display_name + ), + 'class' => 'rowaction' + ) + ); + } + ?> + </span> + <span class="state-icons"><?= $this->serviceFlags($service) ?></span> + </div> + <div class="overview-plugin-output-container"> + <div class="overview-performance-data"> + <?= $this->perfdata($service->service_perfdata, true, 5) ?> + </div> + <p class="overview-plugin-output"><?= $this->pluginOutput($this->ellipsis($service->service_output, 10000), true, $service->service_check_command) ?></p> + </div> + </td> + <?php foreach($this->addColumns as $col): ?> + <?php if ($service->$col && preg_match('~^_(host|service)_([a-zA-Z0-9_]+)$~', $col, $m)): ?> + <td><?= $this->escape(\Icinga\Module\Monitoring\Object\MonitoredObject::protectCustomVars([$m[2] => $service->$col])[$m[2]]) ?></td> + <?php else: ?> + <td><?= $this->escape($service->$col) ?></td> + <?php endif ?> + <?php endforeach ?> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($services->hasMore()): ?> +<div class="dont-print action-links"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ) ?> +</div> +<?php endif ?> +</div> +<?php if (! $this->compact): ?> +<div class="monitoring-statusbar dont-print"> + <?= $this->render('list/components/servicesummary.phtml') ?> + <?= $this->render('list/components/selectioninfo.phtml') ?> +</div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/object/detail-history.phtml b/modules/monitoring/application/views/scripts/object/detail-history.phtml new file mode 100644 index 0000000..692d3e4 --- /dev/null +++ b/modules/monitoring/application/views/scripts/object/detail-history.phtml @@ -0,0 +1,13 @@ +<?php + +if (! $this->compact): ?> +<div class="controls separated"> + <?= $this->tabs ?> +<?php if ($object->type === 'service') { + echo $this->render('partials/object/service-header.phtml'); +} else { + echo $this->render('partials/object/host-header.phtml'); +} ?> +</div> +<?php endif ?> +<?= $this->render('partials/event-history.phtml') ?> diff --git a/modules/monitoring/application/views/scripts/object/detail-tabhook.phtml b/modules/monitoring/application/views/scripts/object/detail-tabhook.phtml new file mode 100644 index 0000000..abcfcc1 --- /dev/null +++ b/modules/monitoring/application/views/scripts/object/detail-tabhook.phtml @@ -0,0 +1,21 @@ +<?php + +if (! $this->compact): ?> +<div class="controls separated"> + <?= $this->tabs ?> +<?php +if ($this->header === true) { + if ($object->type === 'service') { + echo $this->render('partials/object/service-header.phtml'); + } else { + echo $this->render('partials/object/host-header.phtml'); + } +} elseif ($this->header !== false) { + echo $this->header; +} +?> +</div> +<?php endif ?> +<div class="content"> + <?= $this->content ?> +</div>
\ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml b/modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml new file mode 100644 index 0000000..b4e5a9c --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml @@ -0,0 +1,18 @@ +<?php use Icinga\Data\Filter\Filter; ?> +<div class="controls"> +<?php if (! $this->compact): ?> + <?= $this->tabs ?> +<?php endif ?> +<?php if ($object->getType() === $object::TYPE_HOST) { + echo $this->render('partials/object/host-header.phtml'); + $this->baseFilter = Filter::where('host', $object->host_name); + $this->stats = $object->stats; + echo $this->render('list/components/servicesummary.phtml'); +} else { + echo $this->render('partials/object/service-header.phtml'); +} ?> +<?= $this->render('partials/object/quick-actions.phtml') ?> +</div> +<div class="content object-command"> + <?= $form ?> +</div> diff --git a/modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml b/modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml new file mode 100644 index 0000000..8d241ee --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml @@ -0,0 +1,15 @@ +<div class="controls"> +<?php if (! $this->compact): ?> + <?= $tabs ?> +<?php endif ?> +<?php if (isset($serviceStates)): ?> + <?= $this->render('list/components/servicesummary.phtml') ?> + <?= $this->render('partials/service/objects-header.phtml') ?> +<?php else: ?> + <?= $this->render('list/components/hostssummary.phtml') ?> + <?= $this->render('partials/host/objects-header.phtml') ?> +<?php endif ?> +</div> +<div class="content objects-command"> + <?= $form ?> +</div> diff --git a/modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml b/modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml new file mode 100644 index 0000000..f35680c --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml @@ -0,0 +1,24 @@ +<?php +switch ($comment->type) { + case 'flapping': + $icon = 'flapping'; + $title = $this->translate('Flapping'); + $tooltip = $this->translate('Comment was caused by a flapping host or service'); + break; + case 'comment': + $icon = 'user'; + $title = $this->translate('User Comment'); + $tooltip = $this->translate('Comment was created by an user'); + break; + case 'downtime': + $icon = 'plug'; + $title = $this->translate('Downtime'); + $tooltip = $this->translate('Comment was caused by a downtime'); + break; + case 'ack': + $icon = 'ok'; + $title = $this->translate('Acknowledgement'); + $tooltip = $this->translate('Comment was caused by an acknowledgement'); + break; +} +echo $this->icon($icon, $tooltip, array('class' => 'large-icon')); diff --git a/modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml b/modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml new file mode 100644 index 0000000..c603d3c --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml @@ -0,0 +1,82 @@ +<div class="comment-author"> +<?php if ($comment->objecttype === 'service') { + echo '<span class="service-on">'; + echo sprintf( + $this->translate('%s on %s', 'service on host'), + $this->qlink( + $comment->service_display_name, + 'monitoring/service/show', + [ + 'host' => $comment->host_name, + 'service' => $comment->service_description + ], + [ + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $comment->service_display_name, + $comment->host_display_name + ) + ] + ), + $this->qlink( + $comment->host_display_name, + 'monitoring/host/show', + ['host' => $comment->host_name], + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $comment->host_display_name + ) + ] + ) + ); + echo '</span>'; +} else { + echo $this->qlink( + $comment->host_display_name, + 'monitoring/host/show', + array('host' => $comment->host_name), + array( + 'title' => sprintf( + $this->translate('Show detailed information for this comment about host %s'), + $comment->host_display_name + ) + ) + ); +} ?> + <span class="comment-time"> + <?= $this->translate('by') ?> + <?= $this->escape($comment->author) ?> + <?= $this->timeAgo($comment->timestamp) ?> + </span> + <span class="comment-icons" data-base-target="_self"> + <?= $comment->persistent ? $this->icon('attach', 'This comment is persistent') : '' ?> + <?= $comment->expiration ? $this->icon('clock', sprintf( + $this->translate('This comment expires on %s at %s'), + $this->formatDate($comment->expiration), + $this->formatTime($comment->expiration) + )) : '' ?> + <?php if (isset($delCommentForm)) { + // Form is unset if the current user lacks the respective permission + $uniqId = uniqid(); + $buttonId = 'delete-comment-' . $uniqId; + $textId = 'comment-' . $uniqId; + $deleteButton = clone $delCommentForm; + /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm $deleteButton */ + $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action dont-print'); + $deleteButton->populate( + array( + 'comment_id' => $comment->id, + 'comment_is_service' => isset($comment->service_description), + 'comment_name' => $comment->name + ) + ); + $deleteButton->getElement('btn_submit') + ->setAttrib('aria-label', $this->translate('Delete comment')) + ->setAttrib('id', $buttonId) + ->setAttrib('aria-describedby', $buttonId . ' ' . $textId); + echo $deleteButton; + } ?> + </span> +</div> +<?= $this->nl2br($this->markdownLine($comment->comment, isset($textId) ? ['id' => $textId, 'class' => 'caption'] : [ 'class' => 'caption'])) ?> diff --git a/modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml b/modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml new file mode 100644 index 0000000..4472479 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml @@ -0,0 +1,10 @@ +<table> + <tr> + <td class="icon-col"> + <?= $this->render('partials/comment/comment-description.phtml') ?> + </td> + <td> + <?= $this->render('partials/comment/comment-detail.phtml') ?> + </td> + </tr> +</table> diff --git a/modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml b/modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml new file mode 100644 index 0000000..c4c92da --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml @@ -0,0 +1,32 @@ +<table> + <tbody> + <?php + foreach ($comments as $i => $comment): + if ($i === 5) { + break; + } + ?> + <tr> + <td class="icon-col"> + <?= $this->partial('partials/comment/comment-description.phtml', array('comment' => $comment)) ?> + </td> + <td> + <?= $this->partial('partials/comment/comment-detail.phtml', array('comment' => $comment)) ?> + </td> + </tr> + <?php endforeach ?> + </tbody> +</table> +<?php if ($comments->count() > 5): ?> +<p> + <?= $this->qlink( + sprintf($this->translate('List all %d comments'), $comments->count()), + $listAllLink, + null, + array( + 'data-base-target' => '_next', + 'icon' => 'down-open' + ) + ) ?> +</p> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml b/modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml new file mode 100644 index 0000000..dae6caa --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml @@ -0,0 +1,101 @@ +<td class="state-col state-<?= $stateName; ?><?= $downtime->is_in_effect ? ' handled' : ''; ?>"> + <?php if ($downtime->start <= time() && ! $downtime->is_in_effect): ?> + <div class="state-label"><?= $this->translate('ENDS', 'Downtime status'); ?></div> + <div class="state-meta"><?= $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, $this->compact, true) ?></div> + <?php else: ?> + <div class="state-label"><?= $downtime->is_in_effect ? $this->translate('EXPIRES', 'Downtime status') : $this->translate('STARTS', 'Downtime status'); ?></div> + <div class="state-meta"><?= $this->timeUntil($downtime->is_in_effect ? $downtime->end : $downtime->start, $this->compact, true) ?></div> + <?php endif; ?> +</td> +<td> + <div class="comment-author"> + <?php if ($isService) { + echo '<span class="service-on">'; + echo sprintf( + $this->translate('%s on %s', 'service on host'), + $this->qlink( + $downtime->service_display_name, + 'monitoring/service/show', + [ + 'host' => $downtime->host_name, + 'service' => $downtime->service_description + ], + [ + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $downtime->service_display_name, + $downtime->host_display_name + ) + ] + ), + $this->qlink( + $downtime->host_display_name, + 'monitoring/host/show', + ['host' => $downtime->host_name], + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $downtime->host_display_name + ) + ] + ) + ); + echo '</span>'; + } else { + echo $this->qlink( + $downtime->host_display_name, + 'monitoring/host/show', + array('host' => $downtime->host_name, 'downtime_id' => $downtime->id), + array( + 'title' => sprintf( + $this->translate('Show detailed information for this downtime scheduled for host %s'), + $downtime->host_display_name + ) + ) + ); + } ?> + <span class="comment-time"> + <?= $this->escape(sprintf( + $downtime->is_flexible + ? $this->translate('Flexible downtime by %s') + : $this->translate('Fixed downtime by %s'), + $downtime->author_name + )) ?> + </span> + <?php if (! $downtime->is_in_effect && $downtime->start >= time()): ?> + <span><?= sprintf($this->translate('expires %s'), $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, false, true)) ?></span> + <?php endif ?> + <span class="comment-icons"> + <?php if ($downtime->is_flexible): ?> + <?= $this->icon('magic', $this->translate('This downtime is flexible')); ?> + <?php endif ?> + + <?php if ($downtime->is_in_effect): ?> + <?= $this->icon('plug', $this->translate('This downtime is in effect')); ?> + <?php endif ?> + + <?php if (isset($delDowntimeForm)) { + // Form is unset if the current user lacks the respective permission + $uniqId = uniqid(); + $buttonId = 'delete-downtime-' . $uniqId; + $textId = 'downtime-' . $uniqId; + $deleteButton = clone $delDowntimeForm; + /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm $deleteButton */ + $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action dont-print'); + $deleteButton->populate( + array( + 'downtime_id' => $downtime->id, + 'downtime_is_service' => isset($downtime->service_description), + 'downtime_name' => $downtime->name + ) + ); + $deleteButton->getElement('btn_submit') + ->setAttrib('aria-label', $this->translate('Delete downtime')) + ->setAttrib('id', $buttonId) + ->setAttrib('aria-describedby', $buttonId . ' ' . $textId); + echo $deleteButton; + } ?> + </span> + </div> + <?= $this->nl2br($this->markdown($downtime->comment, isset($textId) ? ['id' => $textId] : null)) ?> +</td> diff --git a/modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml b/modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml new file mode 100644 index 0000000..e2582c1 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml @@ -0,0 +1,40 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; +?> +<table class="state-table common-table" data-base-target="_next"> + <tbody> + <?php + foreach ($this->downtimes as $i => $downtime): + if ($i > 5) { + break; + } + if ($downtime->objecttype === 'service') { + $this->isService = true; + $this->stateName = Service::getStateText($downtime->service_state); + } else { + $this->isService = false; + $this->stateName = Host::getStateText($downtime->host_state); + } + $this->downtime = $downtime; + $this->displayComment = false; + ?> + <tr> + <?= $this->render('partials/downtime/downtime-header.phtml') ?> + </tr> + <?php endforeach ?> + </tbody> +</table> +<?php if ($downtimes->count() > 5): ?> +<p> + <?= $this->qlink( + sprintf($this->translate('List all %d downtimes'), $downtimes->count()), + $listAllLink, + null, + array( + 'data-base-target' => '_next', + 'icon' => 'down-open' + ) + ) ?> +</p> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/partials/event-history.phtml b/modules/monitoring/application/views/scripts/partials/event-history.phtml new file mode 100644 index 0000000..b81c95d --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/event-history.phtml @@ -0,0 +1,267 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Url; +use Icinga\Web\UrlParams; + +function contactsLink($match, $view) { + $links = array(); + foreach (preg_split('/,\s/', $match[1]) as $contact) { + $links[] = $view->qlink( + $contact, + 'monitoring/show/contact', + array('contact_name' => $contact), + array('title' => sprintf($view->translate('Show detailed information about %s'), $contact)) + ); + } + return '[' . implode(', ', $links) . ']'; +} + +$self = $this; + +$url = $this->url(); +$limit = (int) $url->getParam('limit', 25); +if (! $url->hasParam('page') || ($page = (int) $url->getParam('page')) < 1) { + $page = 1; +} + +/** @var \Icinga\Module\Monitoring\DataView\EventHistory $history */ +$history->limit($limit * $page); +?> +<div class="content"> +<?php +$dateFormatter = new IntlDateFormatter(setlocale(LC_TIME, 0), IntlDateFormatter::FULL, IntlDateFormatter::NONE); +$lastDate = null; +$flappingMsg = $this->translate('Flapping with a %.2f%% state change rate'); +$rowAction = Url::fromPath('monitoring/event/show'); +?> + <?php foreach ($history->peekAhead() as $event): ?> +<?php if ($lastDate === null): ?> + <table class="table-row-selectable state-table" data-base-target="_next"> + <tbody> +<?php endif; + $icon = ''; + $iconTitle = null; + $isService = isset($event->service_description); + $msg = $event->output; + $stateName = 'no-state'; + + $rowAction->setParams(new UrlParams())->addParams(array( + 'type' => $event->type, + 'id' => $event->id + )); + switch ($event->type) { + case substr($event->type, 0, 13) === 'notification_': + $rowAction->setParam('type', 'notify'); + $icon = 'bell'; + switch (substr($event->type, 13)) { + case 'state': + $iconTitle = $this->translate('State notification', 'tooltip'); + $label = $this->translate('NOTIFICATION'); + $stateName = $isService ? Service::getStateText($event->state) : Host::getStateText($event->state); + break; + case 'ack': + $iconTitle = $this->translate('Ack Notification', 'tooltip'); + $label = $this->translate('ACK NOTIFICATION'); + break; + case 'dt_start': + $iconTitle = $this->translate('Downtime start notification', 'tooltip'); + $label = $this->translate('DOWNTIME START NOTIFICATION'); + break; + case 'dt_end': + $iconTitle = $this->translate('Downtime end notification', 'tooltip'); + $label = $this->translate('DOWNTIME END NOTIFICATION'); + break; + case 'flapping': + $iconTitle = $this->translate('Flapping notification', 'tooltip'); + $label = $this->translate('FLAPPING NOTIFICATION'); + break; + case 'flapping_end': + $iconTitle = $this->translate('Flapping end notification', 'tooltip'); + $label = $this->translate('FLAPPING END NOTIFICATION'); + break; + case 'custom': + $iconTitle = $this->translate('Custom notification', 'tooltip'); + $label = $this->translate('CUSTOM NOTIFICATION'); + break; + } + $msg = $msg ? preg_replace_callback( + '/^\[([^\]]+)\]/', + function($match) use ($self) { return contactsLink($match, $self); }, + $msg + ) : $this->translate('This notification was not sent out to any contact.'); + break; + case 'comment': + $icon = 'comment-empty'; + $iconTitle = $this->translate('Comment', 'tooltip'); + $label = $this->translate('COMMENT'); + break; + case 'comment_deleted': + $icon = 'cancel'; + $iconTitle = $this->translate('Comment removed', 'tooltip'); + $label = $this->translate('COMMENT DELETED'); + break; + case 'ack': + $icon = 'ok'; + $iconTitle = $this->translate('Acknowledged', 'tooltip'); + $label = $this->translate('ACKNOWLEDGED'); + break; + case 'ack_deleted': + $icon = 'ok'; + $iconTitle = $this->translate('Acknowledgement removed', 'tooltip'); + $label = $this->translate('ACKNOWLEDGEMENT REMOVED'); + break; + case 'dt_comment': + $icon = 'plug'; + $iconTitle = $this->translate('Downtime scheduled', 'tooltip'); + $label = $this->translate('SCHEDULED DOWNTIME'); + break; + case 'dt_comment_deleted': + $icon = 'plug'; + $iconTitle = $this->translate('Downtime removed', 'tooltip'); + $label = $this->translate('DOWNTIME DELETED'); + break; + case 'flapping': + $icon = 'flapping'; + $iconTitle = $this->translate('Flapping started', 'tooltip'); + $label = $this->translate('FLAPPING'); + $msg = sprintf($flappingMsg, $msg); + break; + case 'flapping_deleted': + $icon = 'flapping'; + $iconTitle = $this->translate('Flapping stopped', 'tooltip'); + $label = $this->translate('FLAPPING STOPPED'); + $msg = sprintf($flappingMsg, $msg); + break; + case 'hard_state': + if ((int) $event->state === 0) { + $icon = 'thumbs-up'; + } else { + $icon = 'warning-empty'; + } + $iconTitle = $this->translate('Hard state', 'tooltip'); + $label = $isService ? Service::getStateText($event->state, true) : Host::getStateText($event->state, true); + $stateName = $isService ? Service::getStateText($event->state) : Host::getStateText($event->state); + break; + case 'soft_state': + $icon = 'spinner'; + $iconTitle = $this->translate('Soft state', 'tooltip'); + $label = $isService ? Service::getStateText($event->state, true) : Host::getStateText($event->state, true); + $stateName = $isService ? Service::getStateText($event->state) : Host::getStateText($event->state); + break; + case 'dt_start': + $icon = 'plug'; + $iconTitle = $this->translate('Downtime started', 'tooltip'); + $label = $this->translate('DOWNTIME START'); + break; + case 'dt_end': + $icon = 'plug'; + $iconTitle = $this->translate('Downtime ended', 'tooltip'); + $label = $this->translate('DOWNTIME END'); + break; + } ?> + <?php + $currentDate = $dateFormatter->format($event->timestamp); + if ($currentDate !== $lastDate): + $lastDate = $currentDate; + ?> + <tr> + <th colspan="2"><?= $currentDate ?></th> + </tr> + <?php endif ?> + <tr href="<?= $rowAction ?>"> + <td class="state-col state-<?= $stateName ?>"> + <?php if ($history->getIteratorPosition() % $limit === 0): ?> + <a id="page-<?= $history->getIteratorPosition() / $limit + 1 ?>"></a> + <?php endif ?> + <div class="state-label"><?= $this->escape($label) ?></div> + <div class="state-meta"><?= $this->formatTime($event->timestamp) ?></div> + </td> + <td> + <div class="history-message-container"> + <?php if ($icon): ?> + <div class="history-message-icon"> + <?= $this->icon($icon, $iconTitle) ?> + </div> + <?php endif ?> + <div class="history-message-output"> + <?php if ($this->isOverview): ?> + <?php if ($isService) { + echo '<span class="service-on">'; + echo sprintf( + $this->translate('%s on %s', 'service on host'), + $this->qlink( + $event->service_display_name, + 'monitoring/service/show', + [ + 'host' => $event->host_name, + 'service' => $event->service_description + ], + [ + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $event->service_display_name, + $event->host_display_name + ) + ] + ), + $this->qlink( + $event->host_display_name, + 'monitoring/host/show', + ['host' => $event->host_name], + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $event->host_display_name + ) + ] + ) + ); + echo '</span>'; + } else { + echo $this->qlink( + $event->host_display_name, + 'monitoring/host/show', + ['host' => $event->host_name], + [ + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $event->host_display_name + ) + ] + ); + } ?> + <?php endif ?> + <?= $this->nl2br($this->createTicketLinks($this->markdown($msg, ['class' => 'overview-plugin-output']))) ?> + </div> + </div> + </td> + </tr> + <?php endforeach ?> +<?php if ($lastDate !== null): ?> + </tbody> + </table> +<?php endif ?> +<?php if ($history->hasMore()): ?> + <div class="action-links"> + <?php if ($this->compact) { + echo $this->qlink( + $this->translate('Show More'), + $url->without(array('showCompact', 'limit')), + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_next' + ) + ); + } else { + echo $this->qlink( + $this->translate('Load More'), + $url->setAnchor('page-' . ($page + 1)), + array('page' => $page + 1,), + array('class' => 'action-link') + ); + } ?> + </div> +<?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/partials/host/objects-header.phtml b/modules/monitoring/application/views/scripts/partials/host/objects-header.phtml new file mode 100644 index 0000000..48141e2 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/host/objects-header.phtml @@ -0,0 +1,41 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; + +if (! ($hostCount = count($objects))): return; endif ?> +<table class="state-table host-detail-state"> +<tbody> +<?php foreach ($objects as $i => $host): /** @var Host $host */ + if ($i === 5) { + break; + } ?> + <tr> + <td class="state-col state-<?= Host::getStateText($host->host_state); ?><?= $host->host_handled ? ' handled' : '' ?>"> + <span class="sr-only"><?= Host::getStateText($host->host_state) ?></span> + <div class="state-meta"> + <?= $this->timeSince($host->host_last_state_change, $this->compact) ?> + </div> + </td> + <td> + <?= $this->link()->host( + $host->host_name, + $host->host_display_name + ) ?> + <?= $this->hostFlags($host) ?> + </td> + </tr> +<?php endforeach ?> +</tbody> +</table> +<?php if ($hostCount > 5): ?> +<div class="hosts-link"> + <?= $this->qlink( + sprintf($this->translate('List all %d hosts'), $hostCount), + $this->url()->setPath('monitoring/list/hosts'), + null, + array( + 'data-base-target' => '_next', + 'icon' => 'forward' + ) + ) ?> +</div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/partials/object/detail-content.phtml b/modules/monitoring/application/views/scripts/partials/object/detail-content.phtml new file mode 100644 index 0000000..62bfd2c --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/object/detail-content.phtml @@ -0,0 +1,53 @@ +<div class="content" data-base-target="_next"> + <?= $this->render('show/components/output.phtml') ?> + <?= $this->render('show/components/grapher.phtml') ?> + <?= $this->render('show/components/extensions.phtml') ?> + + <h2><?= $this->translate('Problem handling') ?></h2> + <table class="name-value-table"> + <tbody> + <?= $this->render('show/components/acknowledgement.phtml') ?> + <?= $this->render('show/components/comments.phtml') ?> + <?= $this->render('show/components/downtime.phtml') ?> + <?= $this->render('show/components/notes.phtml') ?> + <?= $this->render('show/components/actions.phtml') ?> + <?= $this->render('show/components/flapping.phtml') ?> + <?php if ($object->type === 'service'): ?> + <?= $this->render('show/components/servicegroups.phtml') ?> + <?php else: ?> + <?= $this->render('show/components/hostgroups.phtml') ?> + <?php endif ?> + </tbody> + </table> + + <?= $this->render('show/components/perfdata.phtml') ?> + + <h2><?= $this->translate('Notifications') ?></h2> + <table class="name-value-table"> + <tbody> + <?= $this->render('show/components/notifications.phtml') ?> + <?php if ($this->hasPermission('*') || ! $this->hasPermission('no-monitoring/contacts')): ?> + <?= $this->render('show/components/contacts.phtml') ?> + <?php endif ?> + </tbody> + </table> + + <h2><?= $this->translate('Check execution') ?></h2> + <table class="name-value-table"> + <tbody> + <?= $this->render('show/components/command.phtml') ?> + <?= $this->render('show/components/checksource.phtml') ?> + <?= $this->render('show/components/reachable.phtml') ?> + <?= $this->render('show/components/checkstatistics.phtml') ?> + <?= $this->render('show/components/checktimeperiod.phtml') ?> + </tbody> + </table> + + <?php if (! empty($object->customvars)): ?> + <h2><?= $this->translate('Custom Variables') ?></h2> + <div id="<?= $object->type ?>-customvars" data-visible-height="200" class="collapsible"> + <?= (new \Icinga\Module\Monitoring\Web\Widget\CustomVarTable($object->customvarsWithOriginalNames, $object)) ?> + </div> + <?php endif ?> + <?= $this->render('show/components/flags.phtml') ?> +</div> diff --git a/modules/monitoring/application/views/scripts/partials/object/host-header.phtml b/modules/monitoring/application/views/scripts/partials/object/host-header.phtml new file mode 100644 index 0000000..4de4a01 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/object/host-header.phtml @@ -0,0 +1,51 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Web\Url; + +/** @var Host $object */ + +$url = Url::fromRequest(); +$linkHostName = ! ($url->getPath() === 'monitoring/host/show' && $url->getParam('host') === $object->host_name); +?> +<table class="state-table host-detail-state"> + <tr> + <td class="state-col state-<?= Host::getStateText($object->host_state) ?><?= $object->host_handled ? ' handled' : '' ?>"> + <div class="state-header"><?= Host::getStateText($object->host_state, true) ?></div> + <div class="state-meta"> + <?= $this->timeSince($object->host_last_state_change) ?> + <?php if ((int) $object->host_state > 0 && (int) $object->host_state_type === 0): ?> + <div><?= $this->translate('Soft', 'Soft state') ?> <?= $object->host_attempt ?></div> + <?php endif ?> + </div> + </td> + <td> + <?= $this->iconImage()->host($object) ?> + <?php + if ($linkHostName) { + echo '<a href="' . Url::fromPath('monitoring/host/show', array('host' => $object->host_name)) . '">'; + } + ?> + <span class="selectable"><strong><?= $this->escape($object->host_display_name) ?></strong></span> + <?php if ($object->host_display_name !== $object->host_name): ?> + <span class="selectable host-meta">(<?= $this->escape($object->host_name) ?>)</span> + <?php endif ?> + <?php + if ($linkHostName) { + echo '</a>'; + } + ?> + <?php if ($object->host_alias !== $object->host_display_name && $object->host_alias !== $object->host_name): ?> + <div class="selectable host-meta"> + <?= $this->escape($this->translate('Alias', 'host') . ': ' . $object->host_alias) ?> + </div> + <?php endif ?> + <?= $this->hostFlags($object) ?> + <?php if ($object->host_address6 && $object->host_address6 !== $object->host_name): ?> + <div class="selectable host-meta" title="<?= $this->translate('IPv6 address') ?>"><?= $this->escape($object->host_address6) ?></div> + <?php endif ?> + <?php if ($object->host_address && $object->host_address !== $object->host_name): ?> + <div class="selectable host-meta" title="<?= $this->translate('IPv4 address') ?>"><?= $this->escape($object->host_address) ?></div> + <?php endif ?> + </td> + </tr> +</table> diff --git a/modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml b/modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml new file mode 100644 index 0000000..fe05a84 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml @@ -0,0 +1,144 @@ +<div class="quick-actions"> + <ul class="nav tab-nav"> + <?php if (isset($removeAckForm)): ?> + <li> + <?php + $removeAckForm = clone $removeAckForm; + $removeAckForm->setAttrib('id', 'quickAction_' . $removeAckForm->getName()); // Avoids id duplication + $removeAckForm->setLabelEnabled(true); + echo $removeAckForm; + ?> + </li> + <?php elseif /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ ($this->hasPermission('monitoring/command/acknowledge-problem') && ! (in_array((int) $object->state, array(0, 99))) ): ?> + <li> + <?php if ($object->getType() === $object::TYPE_HOST) { + echo $this->qlink( + $this->translate('Acknowledge'), + 'monitoring/host/acknowledge-problem', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'edit', + 'title' => $this->translate( + 'Acknowledge this problem, suppress all future notifications for it and tag it as being handled' + ) + ) + ); + } else { + echo $this->qlink( + $this->translate('Acknowledge'), + 'monitoring/service/acknowledge-problem', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'edit', + 'title' => $this->translate( + 'Acknowledge this problem, suppress all future notifications for it and tag it as being handled' + ) + ) + ); + } ?> + </li> + <?php endif ?> + <?php if (isset($checkNowForm)): // Form is unset if the current user lacks the respective permission ?> + <?php ($checkNowForm = clone $checkNowForm)->setAttrib('id', 'quickAction_' . $checkNowForm->getName()); // Avoids id duplication ?> + <li><?= $checkNowForm ?></li> + <?php endif ?> + <?php if ($this->hasPermission('monitoring/command/comment/add')): ?> + <li> + <?php if ($object->getType() === $object::TYPE_HOST) { + echo $this->qlink( + $this->translate('Comment'), + 'monitoring/host/add-comment', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'comment-empty', + 'title' => $this->translate('Add a new comment to this host') + ) + ); + } else { + echo $this->qlink( + $this->translate('Comment'), + 'monitoring/service/add-comment', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'comment-empty', + 'title' => $this->translate('Add a new comment to this service') + ) + ); + } ?> + </li> + <?php endif ?> + <?php if ($this->hasPermission('monitoring/command/send-custom-notification')): ?> + <li> + <?php if ($object->getType() === $object::TYPE_HOST) { + echo $this->qlink( + $this->translate('Notification'), + 'monitoring/host/send-custom-notification', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'bell', + 'title' => $this->translate( + 'Send a custom notification to contacts responsible for this host' + ) + ) + ); + } else { + echo $this->qlink( + $this->translate('Notification'), + 'monitoring/service/send-custom-notification', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'bell', + 'title' => $this->translate( + 'Send a custom notification to contacts responsible for this service' + ) + ) + ); + } ?> + </li> + <?php endif ?> + <?php if ($this->hasPermission('monitoring/command/downtime/schedule')): ?> + <li><?php if ($object->getType() === $object::TYPE_HOST) { + echo $this->qlink( + $this->translate('Downtime'), + 'monitoring/host/schedule-downtime', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'plug', + 'title' => $this->translate( + 'Schedule a downtime to suppress all problem notifications within a specific period of time' + ) + ) + ); + } else { + echo $this->qlink( + $this->translate('Downtime'), + 'monitoring/service/schedule-downtime', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'plug', + 'title' => $this->translate( + 'Schedule a downtime to suppress all problem notifications within a specific period of time' + ) + ) + ); + } ?> + </li> + <?php endif ?> + </ul> +</div> diff --git a/modules/monitoring/application/views/scripts/partials/object/service-header.phtml b/modules/monitoring/application/views/scripts/partials/object/service-header.phtml new file mode 100644 index 0000000..318fe49 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/object/service-header.phtml @@ -0,0 +1,72 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Url; + +/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + +$url = Url::fromRequest(); +$linkServiceName = ! ($url->getPath() === 'monitoring/service/show' && $url->getParam('service') === $object->service_description); +?> +<table class="state-table service-detail-state"> + <tr> + <td class="state-col state-<?= Host::getStateText($object->host_state) ?><?= $object->host_handled ? ' handled' : '' ?>"> + <div class="state-label"><?= Host::getStateText($object->host_state, true) ?></div> + <div class="state-meta"> + <?= $this->timeSince($object->host_last_state_change) ?> + <?php if ((int) $object->host_state > 0 && (int) $object->host_state_type === 0): ?> + <div><?= $this->translate('Soft', 'Soft state') ?> <?= $object->host_attempt ?></div> + <?php endif ?> + </div> + </td> + <td> + <?= $this->iconImage()->host($object) ?> + <a href="<?= Url::fromPath('monitoring/host/show', array('host' => $object->host_name)) ?>"> + <span class="selectable"><strong><?= $this->escape($object->host_display_name) ?></strong></span> + <?php if ($object->host_display_name !== $object->host_name): ?> + <span class="selectable host-meta">(<?= $this->escape($object->host_name) ?>)</span> + <?php endif ?> + </a> + <?= $this->hostFlags($object) ?> + <?php if ($object->host_address6 && $object->host_address6 !== $object->host_name): ?> + <div class="selectable host-meta" title="<?= $this->translate('IPv6 address') ?>"><?= $this->escape($object->host_address6) ?></div> + <?php endif ?> + <?php if ($object->host_address && $object->host_address !== $object->host_name): ?> + <div class="selectable host-meta" title="<?= $this->translate('IPv4 address') ?>"><?= $this->escape($object->host_address) ?></div> + <?php endif ?> + </td> + </tr> + <tr> + <td class="state-col state-<?= Service::getStateText($object->service_state) ?><?= $object->service_handled ? ' handled' : '' ?>"> + <div class="state-label"><?= Service::getStateText($object->service_state, true) ?></div> + <div class="state-meta"> + <?= $this->timeSince($object->service_last_state_change) ?> + <?php if ((int) $object->service_state > 0 && (int) $object->service_state_type === 0): ?> + <div><?= $this->translate('Soft', 'Soft state') ?> <?= $object->service_attempt ?></div> + <?php endif ?> + </div> + </td> + <td> + <?= $this->iconImage()->service($object) ?> + <?= $this->translate('Service') ?>: + <?php + if ($linkServiceName) { + echo '<a href="' . Url::fromPath('monitoring/service/show', array( + 'host' => $object->host_name, + 'service' => $object->service_description + )) . '">'; + } + ?> + <span class="selectable"><strong><?= $this->escape($object->service_display_name) ?></strong></span> + <?php if ($object->service_display_name !== $object->service_description): ?> + <span class="selectable service-meta">(<?= $this->escape($object->service_description) ?>)</span> + <?php endif ?> + <?php + if ($linkServiceName) { + echo '</a>'; + } + ?> + <?= $this->serviceFlags($object) ?> + </td> + </tr> +</table> diff --git a/modules/monitoring/application/views/scripts/partials/service/objects-header.phtml b/modules/monitoring/application/views/scripts/partials/service/objects-header.phtml new file mode 100644 index 0000000..d342d87 --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/service/objects-header.phtml @@ -0,0 +1,45 @@ +<?php +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\Service; + +if (! ($serviceCount = count($objects))): return; endif ?> +<table class="state-table service-detail-state"> +<tbody> +<?php foreach ($objects as $i => $service): /** @var Service $service */ + if ($i === 5) { + break; + } ?> + <tr> + <td class="state-col state-<?= Service::getStateText($service->service_state) ?><?= $service->service_handled ? ' handled' : '' ?>"> + <span class="sr-only"><?= Service::getStateText($service->service_state) ?></span> + <div class="state-meta"> + <?= $this->timeSince($service->service_last_state_change, $this->compact) ?> + </div> + </td> + <td> + <?= $this->link()->service( + $service->service_description, + $service->service_display_name, + $service->host_name, + $service->host_display_name + . ($service->host_state != 0 ? ' (' . Host::getStateText($service->host_state, true) . ')' : '') + ) ?> + <?= $this->serviceFlags($service) ?> + </td> + </tr> +<?php endforeach ?> +</tbody> +</table> +<?php if ($serviceCount > 5): ?> +<div class="services-link"> + <?= $this->qlink( + sprintf($this->translate('List all %d services'), $serviceCount), + $this->url()->setPath('monitoring/list/services'), + null, + array( + 'data-base-target' => '_next', + 'icon' => 'forward' + ) + ) ?> +</div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/partials/show-more.phtml b/modules/monitoring/application/views/scripts/partials/show-more.phtml new file mode 100644 index 0000000..fd6a99d --- /dev/null +++ b/modules/monitoring/application/views/scripts/partials/show-more.phtml @@ -0,0 +1,15 @@ +<?php +/** @var \Icinga\Module\Monitoring\DataView\DataView $dataView */ +if ($dataView->hasMore()): ?> +<div class="text-right"> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('showCompact', 'limit')), + null, + array( + 'data-base-target' => '_next', + 'class' => 'action-link' + ) + ) ?> +</div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/service/show.phtml b/modules/monitoring/application/views/scripts/service/show.phtml new file mode 100644 index 0000000..bc9c612 --- /dev/null +++ b/modules/monitoring/application/views/scripts/service/show.phtml @@ -0,0 +1,8 @@ +<div class="controls controls-separated"> +<?php if (! $this->compact): ?> + <?= $this->tabs ?> +<?php endif ?> + <?= $this->render('partials/object/service-header.phtml') ?> + <?= $this->render('partials/object/quick-actions.phtml') ?> +</div> +<?= $this->render('partials/object/detail-content.phtml') ?> diff --git a/modules/monitoring/application/views/scripts/services/show.phtml b/modules/monitoring/application/views/scripts/services/show.phtml new file mode 100644 index 0000000..e9fb56f --- /dev/null +++ b/modules/monitoring/application/views/scripts/services/show.phtml @@ -0,0 +1,208 @@ +<div class="controls"> + + <?php if (! $this->compact): ?> + <?= $tabs ?> + <?php endif ?> + <?= $this->render('list/components/servicesummary.phtml') ?> + <?= $this->render('partials/service/objects-header.phtml') ?> + <?php + $serviceCount = count($objects); + $unhandledCount = count($unhandledObjects); + $problemCount = count($problemObjects); + $unackCount = count($unacknowledgedObjects); + $scheduledDowntimeCount = count($objects->getScheduledDowntimes()); + ?> +</div> + +<div class="content"> + + <?php if ($serviceCount === 0): ?> + <?= $this->translate('No services found matching the filter') ?> + <?php else: ?> + <?= $this->render('show/components/extensions.phtml') ?> + <h2> <?= $this->translate('Problem handling') ?> </h2> + <table class="name-value-table"> + <tbody> + <?php if ($unackCount > 0): ?> + <tr> + <th> <?= sprintf($this->translate('%d unhandled problems'), $unackCount) ?> </th> + <td> <?= $this->qlink( + $this->translate('Acknowledge'), + $acknowledgeLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'check' + ) + ) ?> </td> + </tr> + <?php endif; ?> + + <?php if (($acknowledgedCount = count($acknowledgedObjects)) > 0): ?> + <tr> + <th> <?= sprintf( + $this->translatePlural( + '%s acknowledgement', + '%s acknowledgements', + $acknowledgedCount + ), + '<b>' . $acknowledgedCount . '</b>' + ) ?> + </th> + <td> + <?= $removeAckForm->setLabelEnabled(true) ?> + </td> + </tr> + <?php endif ?> + + <tr> + <th> <?= $this->translate('Comments') ?> </th> + <td> + <?= $this->qlink( + $this->translate('Add comments'), + $addCommentLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'comment-empty' + ) + ) ?> + </td> + </tr> + + <?php if (($commentCount = count($objects->getComments())) > 0): ?> + <tr> + <th></th> + <td> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%s comment', + '%s comments', + $commentCount + ), + $commentCount + ), + $commentsLink, + null, + array('data-base-target' => '_next') + ) ?> + </td> + </tr> + <?php endif ?> + + <tr> + <th> + <?= $this->translate('Downtimes') ?> + </th> + <td> + <?= $this->qlink( + $this->translate('Schedule downtimes'), + $downtimeAllLink, + null, + array( + 'icon' => 'plug', + 'class' => 'action-link' + ) + ) ?> + </td> + </tr> + + <?php if ($scheduledDowntimeCount > 0): ?> + <tr> + <th></th> + <td> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%d scheduled downtime', + '%d scheduled downtimes', + $scheduledDowntimeCount + ), + $scheduledDowntimeCount + ), + $showDowntimesLink, + null, + array( + 'data-base-target' => '_next' + ) + ) ?> + </td> + </tr> + <?php endif ?> + + </tbody> + </table> + + <?php if ($this->hasPermission('monitoring/command/send-custom-notification')): ?> + + <h2> <?= $this->translate('Notifications') ?> </h2> + + <table class="name-value-table"> + <tbody> + <tr> + <th> <?= $this->translate('Notifications') ?> </th> + <td> + <?= $this->qlink( + $this->translate('Send notifications'), + $sendCustomNotificationLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'bell' + ) + ) ?> + </td> + </tr> + </tbody> + </table> + <?php endif ?> + + <h2> <?= $this->translate('Check Execution') ?> </h2> + + <table class="name-value-table"> + <tbody> + <tr> + <th> <?= $this->translate('Command') ?> </th> + <td> + <?= $this->qlink( + $this->translate('Process check result'), + $processCheckResultAllLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'edit' + ) + ) ?> + </td> + </tr> + + <?php if (isset($checkNowForm)): // Form is unset if the current user lacks the respective permission ?> + <tr> + <th> <?= $this->translate('Schedule Check') ?> </th> + <td> <?= $checkNowForm ?> </td> + </tr> + <?php endif ?> + + <?php if (isset($rescheduleAllLink)): ?> + <tr> + <th></th> + <td> + <?= $this->qlink( + $this->translate('Reschedule'), + $rescheduleAllLink, + null, + array( + 'class' => 'action-link', + 'icon' => 'calendar-empty' + ) + ) ?> + </td> + </tr> + <?php endif ?> + </tbody> + </table> + <h2><?= $this->translate('Feature Commands') ?></h2> + <?= $toggleFeaturesForm ?> + <?php endif ?> +</div> diff --git a/modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml b/modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml new file mode 100644 index 0000000..fd7f6bb --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml @@ -0,0 +1,94 @@ +<?php + +/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + +if (in_array((int) $object->state, array(0, 99))) { + // Ignore this markup if the object is in a non-problem state or pending + return; +} + +if ($object->acknowledged): +$acknowledgement = $object->acknowledgement; +/** @var \Icinga\Module\Monitoring\Object\Acknowledgement $acknowledgement */ +?> +<tr> + <th><?= $this->translate('Acknowledged') ?></th> + <td data-base-target="_self"> + <?php if ($acknowledgement): ?> + <dl class="comment-list"> + <dt> + <?= $this->escape($acknowledgement->getAuthor()) ?> + <span class="comment-time"> + <?= $this->translate('acknowledged') ?> + <?= $this->timeAgo($acknowledgement->getEntryTime()) ?> + <?php if ($acknowledgement->expires()): ?> + <span aria-hidden="true">ǀ</span> + <?= sprintf( + $this->translate('Expires %s'), + $this->timeUntil($acknowledgement->getExpirationTime()) + ) ?> + <?php endif ?> + </span> + <?php if ($acknowledgement->getSticky()): ?> + <?= $this->icon('pin', sprintf( + $this->translate( + 'Acknowledgement remains until the %1$s recovers even if the %1$s changes state' + ), + $object->getType(true) + )) ?> + <?php endif ?> + <?php if (isset($removeAckForm)) { + // Form is unset if the current user lacks the respective permission + $removeAckForm->setAttrib('class', $removeAckForm->getAttrib('class') . ' remove-action'); + echo $removeAckForm; + } ?> + </dt> + <dd> + <?= $this->nl2br($this->createTicketLinks($this->markdown($acknowledgement->getComment()))) ?> + </dd> + </dl> + <?php elseif (isset($removeAckForm)): ?> + <?= $removeAckForm ?> + <?php endif ?> + </td> +</tr> +<?php else: ?> +<tr> + <th><?= $this->translate('Not acknowledged') ?></th> + <td> + <?php if ($this->hasPermission('monitoring/command/acknowledge-problem')) { + if ($object->getType() === $object::TYPE_HOST) { + $ackLink = $this->href( + 'monitoring/host/acknowledge-problem', + array('host' => $object->getName()), + null, + array('class' => 'action-link') + ); + } else { + $ackLink = $this->href( + 'monitoring/service/acknowledge-problem', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + null, + array('class' => 'action-link') + ); + } + ?> + <?= $this->qlink( + $this->translate('Acknowledge'), + $ackLink, + null, + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'edit', + 'title' => $this->translate( + 'Acknowledge this problem, suppress all future notifications for it and tag it as being handled' + ) + ) + ) ?> + <?php } else { + echo '-'; + } // endif ?> + </td> +</tr> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/show/components/actions.phtml b/modules/monitoring/application/views/scripts/show/components/actions.phtml new file mode 100644 index 0000000..938ab2a --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/actions.phtml @@ -0,0 +1,43 @@ +<?php + +use Icinga\Web\Navigation\Navigation; + +$navigation = new Navigation(); +$navigation->load($object->getType() . '-action'); +foreach ($navigation as $item) { + $item->setObject($object); +} + +foreach ($object->getActionUrls() as $i => $link) { + $navigation->addItem( + + // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201 + $this->icon( + 'forward', + $this->translate('Link opens in new window'), + array('aria-label' => $this->translate('Link opens in new window')) + ) . ' Action ' . ($i + 1), + array( + 'url' => $link, + 'target' => '_blank', + 'renderer' => array( + 'NavigationItemRenderer', + 'escape_label' => false + ) + ) + ); +} + +if (isset($this->actions)) { + $navigation->merge($this->actions); +} + +if ($navigation->isEmpty() || ! $navigation->hasRenderableItems()) { + return; +} + +?> +<tr> + <th><?= $this->translate('Actions'); ?></th> + <?= $navigation->getRenderer()->setElementTag('td')->setCssClass('actions go-ahead'); ?> +</tr> diff --git a/modules/monitoring/application/views/scripts/show/components/checksource.phtml b/modules/monitoring/application/views/scripts/show/components/checksource.phtml new file mode 100644 index 0000000..ac9799f --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/checksource.phtml @@ -0,0 +1,6 @@ +<?php if ($object->check_source !== null): ?> +<tr> + <th><?= $this->translate('Check Source') ?></th> + <td><?= $this->escape($object->check_source) ?></td> +</tr> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml b/modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml new file mode 100644 index 0000000..e37e30a --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml @@ -0,0 +1,85 @@ +<?php +/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ +$activeChecksEnabled = (bool) $object->active_checks_enabled; +?> + +<tr> + <th><?= $activeChecksEnabled ? $this->translate('Last check') : $this->translate('Last update') ?></th> + <td data-base-target="_self"> +<?php if ((int) $object->state !== 99): ?> + <?= $this->timeAgo($object->last_check) ?> + <?php if ($object->next_update < time()): ?> + <?= $this->icon('circle', $this->translate('Check result is late'), array('class' => 'icon-stateful state-critical')) ?> + <?php endif ?> +<?php endif ?> + <?php if (isset($checkNowForm)) { // Form is unset if the current user lacks the respective permission + echo $checkNowForm; + } ?> + </td> +</tr> + +<tr> + <th><?= $activeChecksEnabled ? $this->translate('Next check') : $this->translate('Next update') ?></th> + <td> + <?php if ((int) $object->state !== 99) { + if ($activeChecksEnabled) { + echo $this->timeUntil($object->next_check); + } else { + echo sprintf($this->translate('expected %s'), $this->timeUntil($object->next_update)); + } + } ?> + <?php if ($activeChecksEnabled && $this->hasPermission('monitoring/command/schedule-check')) { + if ($object->getType() === $object::TYPE_SERVICE) { + echo $this->qlink( + $this->translate('Reschedule'), + 'monitoring/service/reschedule-check', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'calendar-empty', + 'title' => $this->translate( + 'Schedule the next active check at a different time than the current one' + ) + ) + ); + } else { + echo $this->qlink( + $this->translate('Reschedule'), + 'monitoring/host/reschedule-check', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'calendar-empty', + 'title' => $this->translate( + 'Schedule the next active check at a different time than the current one' + ) + ) + ); + } + } ?> + </td> +</tr> + +<tr> + <th><?= $this->translate('Check attempts') ?></th> + <td> + <?= $object->attempt ?> + (<?= (int) $object->state_type === 0 ? $this->translate('soft state') : $this->translate('hard state') ?>) + </td> +</tr> + +<?php if ($object->check_execution_time): ?> +<tr> + <th><?= $this->translate('Check execution time') ?></th> + <td><?= round((float) $object->check_execution_time, 3) ?>s</td> +</tr> +<?php endif ?> + +<?php if ($object->check_latency): ?> +<tr> + <th><?= $this->translate('Check latency') ?></th> + <td><?= $object->check_latency ?>s</td> +</tr> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml b/modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml new file mode 100644 index 0000000..34c4eb9 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml @@ -0,0 +1,21 @@ +<?php if (isset($object->service_check_timeperiod)): ?> + +<tr> + <th><?= $this->translate('Check Timeperiod') ?></th> + <td> + <?= $object->service_check_timeperiod ?> + </td> +</tr> + +<?php endif ?> + +<?php if (isset($object->host_check_timeperiod)): ?> + + <tr> + <th><?= $this->translate('Check Timeperiod') ?></th> + <td> + <?= $object->host_check_timeperiod ?> + </td> + </tr> + +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/show/components/command.phtml b/modules/monitoring/application/views/scripts/show/components/command.phtml new file mode 100644 index 0000000..9b51458 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/command.phtml @@ -0,0 +1,52 @@ +<?php +$parts = explode('!', $object->check_command); +$command = array_shift($parts); + +if ($showInstance): ?> +<tr> + <th><?= $this->translate('Instance') ?></th> + <td><?= $this->escape($object->instance_name) ?></td> +</tr> +<?php endif ?> +<tr> + <th><?= $this->translate('Command') ?></th> + <td> + <?= $this->escape($command) ?> + <?php if ($this->hasPermission('monitoring/command/process-check-result') && $object->passive_checks_enabled) { + $title = sprintf( + $this->translate('Submit a one time or so called passive result for the %s check'), $command + ); + if ($object->getType() === $object::TYPE_HOST) { + echo $this->qlink( + $this->translate('Process check result'), + 'monitoring/host/process-check-result', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'edit', + 'title' => $title + ) + ); + } else { + echo $this->qlink( + $this->translate('Process check result'), + 'monitoring/service/process-check-result', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'edit', + 'title' => $title + ) + ); + } + } ?> + </td> +</tr> + +<?php +$row = "<tr>\n <th>%s</th>\n <td>%s</td>\n</tr>\n"; +for ($i = 0; $i < count($parts); $i++) { + printf($row, '$ARG' . ($i + 1) . '$', $this->escape($parts[$i])); +} diff --git a/modules/monitoring/application/views/scripts/show/components/comments.phtml b/modules/monitoring/application/views/scripts/show/components/comments.phtml new file mode 100644 index 0000000..fd980ee --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/comments.phtml @@ -0,0 +1,86 @@ +<?php +$addLink = false; +if ($this->hasPermission('monitoring/command/comment/add')) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + if ($object->getType() === $object::TYPE_HOST) { + $addLink = $this->qlink( + $this->translate('Add comment'), + 'monitoring/host/add-comment', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'comment-empty', + 'title' => $this->translate('Add a new comment to this host') + ) + ); + } else { + $addLink = $this->qlink( + $this->translate('Add comment'), + 'monitoring/service/add-comment', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'comment-empty', + 'title' => $this->translate('Add a new comment to this service') + ) + ); + } +} +if (empty($object->comments) && ! $addLink) { + return; +} +?> +<tr> + <th><?php + echo $this->translate('Comments'); + if (! empty($object->comments) && $addLink) { + echo '<br>' . $addLink; + } + ?></th> + <td data-base-target="_self"> + <?php if (empty($object->comments)): + echo $addLink; + else: ?> + <dl class="comment-list"> + <?php foreach ($object->comments as $comment): ?> + <dt> + <a data-base-target="_next" href="<?= $this->href('monitoring/comment/show', array('comment_id' => $comment->id)) ?>"> + <?= $this->escape($comment->author) ?> + <span class="comment-time"> + <?= $this->translate('commented') ?> + <?= $this->timeAgo($comment->timestamp) ?> + <?php if ($comment->expiration): ?> + <span aria-hidden="true">ǀ</span> + <?= sprintf( + $this->translate('Expires %s'), + $this->timeUntil($comment->expiration) + ) ?> + <?php endif ?> + </span> + </a> + <?= $comment->persistent ? $this->icon('attach', 'This comment is persistent.') : '' ?> + <?php if (isset($delCommentForm)) { + // Form is unset if the current user lacks the respective permission + $deleteButton = clone($delCommentForm); + /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm $deleteButton */ + $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action'); + $deleteButton->populate( + array( + 'comment_id' => $comment->id, + 'comment_is_service' => isset($comment->service_description), + 'comment_name' => $comment->name + ) + ); + echo $deleteButton; + } ?> + </dt> + <dd> + <?= $this->nl2br($this->createTicketLinks($this->markdownLine($comment->comment, [ 'class' => 'caption']))) ?> + </dd> + <?php endforeach ?> + </dl> + <?php endif ?> + </td> +</tr> diff --git a/modules/monitoring/application/views/scripts/show/components/contacts.phtml b/modules/monitoring/application/views/scripts/show/components/contacts.phtml new file mode 100644 index 0000000..5661c1a --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/contacts.phtml @@ -0,0 +1,38 @@ +<?php + +if ($object->contacts->hasResult()) { + + $list = array(); + foreach ($object->contacts as $contact) { + $list[] = $this->qlink( + $contact->contact_alias, + 'monitoring/show/contact', + array('contact_name' => $contact->contact_name), + array('title' => sprintf($this->translate('Show detailed information about %s'), $contact->contact_alias)) + ); + } + + printf( + "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n", + $this->translate('Contacts'), + implode(', ', $list) + ); +} + +if ($object->contactgroups->hasResult()) { + $list = array(); + foreach ($object->contactgroups as $contactgroup) { + $list[] = $this->qlink( + $contactgroup->contactgroup_alias, + 'monitoring/list/contactgroups', + array('contactgroup_name' => $contactgroup->contactgroup_name), + array('title' => sprintf($this->translate('List contacts in contact-group "%s"'), $contactgroup->contactgroup_alias)) + ); + } + + printf( + "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n", + $this->translate('Contactgroups'), + implode(', ', $list) + ); +} diff --git a/modules/monitoring/application/views/scripts/show/components/downtime.phtml b/modules/monitoring/application/views/scripts/show/components/downtime.phtml new file mode 100644 index 0000000..618d4d9 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/downtime.phtml @@ -0,0 +1,109 @@ +<?php +$addLink = false; +if ($this->hasPermission('monitoring/command/downtime/schedule')) { + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + if ($object->getType() === $object::TYPE_HOST) { + $addLink = $this->qlink( + $this->translate('Schedule downtime'), + 'monitoring/host/schedule-downtime', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'plug', + 'title' => $this->translate( + 'Schedule a downtime to suppress all problem notifications within a specific period of time' + ) + ) + ); + } else { + $addLink = $this->qlink( + $this->translate('Schedule downtime'), + 'monitoring/service/schedule-downtime', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'plug', + 'title' => $this->translate( + 'Schedule a downtime to suppress all problem notifications within a specific period of time' + ) + ) + ); + } +} +if (empty($object->downtimes) && ! $addLink) { + return; +} +?> +<tr> + <th><?php + echo $this->translate('Downtimes'); + if (! empty($object->downtimes) && $addLink) { + echo '<br>' . $addLink; + } + ?></th> + <td data-base-target="_self"> + <?php if (empty($object->downtimes)): + echo $addLink; + else: ?> + <dl class="comment-list"> + <?php foreach ($object->downtimes as $downtime): + if ((bool) $downtime->is_in_effect) { + $state = sprintf( + $this->translate('expires %s', 'Last format parameter represents the downtime expire time'), + $this->timeUntil($downtime->end, false, true) + ); + } else { + if ($downtime->start <= time()) { + $state = sprintf( + $this->translate('ends %s', 'Last format parameter represents the end time'), + $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, false, true) + ); + } else { + $state = sprintf( + $this->translate('scheduled %s', 'Last format parameter represents the time scheduled'), + $this->timeUntil($downtime->start, false, true) + ) . ' ' . sprintf( + $this->translate('expires %s', 'Last format parameter represents the downtime expire time'), + $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, false, true) + ); + } + } + ?> + <dt> + <?= $this->escape(sprintf( + $downtime->is_flexible + ? $this->translate('Flexible downtime by %s') + : $this->translate('Fixed downtime by %s'), + $downtime->author_name + )) ?> + <span class="comment-time"> + <?= $state ?> + <span aria-hidden="true">ǀ</span> + <?= $this->translate('created') ?> + <?= $this->timeAgo($downtime->entry_time) ?> + </span> + <?php if (isset($delDowntimeForm)) { + // Form is unset if the current user lacks the respective permission + $deleteButton = clone($delDowntimeForm); + /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm $deleteButton */ + $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action'); + $deleteButton->populate( + array( + 'downtime_id' => $downtime->id, + 'downtime_is_service' => $object->getType() === $object::TYPE_SERVICE, + 'downtime_name' => $downtime->name + ) + ); + echo $deleteButton; + } ?> + </dt> + <dd> + <?= $this->nl2br($this->createTicketLinks($this->markdown($downtime->comment))) ?> + </dd> + <?php endforeach ?> + </dl> + <?php endif ?> + </td> +</tr> diff --git a/modules/monitoring/application/views/scripts/show/components/extensions.phtml b/modules/monitoring/application/views/scripts/show/components/extensions.phtml new file mode 100644 index 0000000..263b7e4 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/extensions.phtml @@ -0,0 +1,4 @@ +<?php +foreach ($extensionsHtml as $extensionHtml) { + echo $extensionHtml; +} diff --git a/modules/monitoring/application/views/scripts/show/components/flags.phtml b/modules/monitoring/application/views/scripts/show/components/flags.phtml new file mode 100644 index 0000000..871a4dd --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/flags.phtml @@ -0,0 +1,4 @@ +<div data-base-target="_self"> + <h2><?= $this->translate('Feature Commands') ?></h2> + <?= $toggleFeaturesForm ?> +</div> diff --git a/modules/monitoring/application/views/scripts/show/components/flapping.phtml b/modules/monitoring/application/views/scripts/show/components/flapping.phtml new file mode 100644 index 0000000..f09b107 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/flapping.phtml @@ -0,0 +1,14 @@ +<?php + +if ($object->is_flapping) { + printf( + "<tr><th>%s</th><td>%s %s</td></tr>\n", + 'Flapping', + $this->icon('flapping', 'Flapping'), + sprintf( + 'Currently flapping with a %.2f%% state change rate', + $object->percent_state_change + ) + ); +} + diff --git a/modules/monitoring/application/views/scripts/show/components/grapher.phtml b/modules/monitoring/application/views/scripts/show/components/grapher.phtml new file mode 100644 index 0000000..0b49e63 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/grapher.phtml @@ -0,0 +1,6 @@ +<?php if (isset($graphers)) { + foreach ($graphers as $grapher) { + echo $grapher->getPreviewHtml($object); + } +} ?> + diff --git a/modules/monitoring/application/views/scripts/show/components/hostgroups.phtml b/modules/monitoring/application/views/scripts/show/components/hostgroups.phtml new file mode 100644 index 0000000..377b56f --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/hostgroups.phtml @@ -0,0 +1,19 @@ +<?php + +if (empty($object->hostgroups)) return; + +$list = array(); +foreach ($object->hostgroups as $name => $alias) { + $list[] = $this->qlink( + $alias, + 'monitoring/list/hosts', + array('hostgroup_name' => $name), + array('title' => sprintf($this->translate('List all hosts in the group "%s"'), $alias)) + ); +} +printf( + "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n", + $this->translate('Hostgroups'), + implode(', ', $list) +); + diff --git a/modules/monitoring/application/views/scripts/show/components/notes.phtml b/modules/monitoring/application/views/scripts/show/components/notes.phtml new file mode 100644 index 0000000..c868c95 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/notes.phtml @@ -0,0 +1,48 @@ +<?php + +use Icinga\Web\Navigation\Navigation; + +/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + +$navigation = new Navigation(); +$notes = trim($object->notes); + +$links = $object->getNotesUrls(); +if (! empty($links)) { + foreach ($links as $link) { + $navigation->addItem( + // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201 + $this->icon( + 'forward', + $this->translate('Link opens in new window'), + array('aria-label' => $this->translate('Link opens in new window')) + ) . ' ' . $this->escape($link), + array( + 'url' => $link, + 'target' => '_blank', + 'renderer' => array( + 'NavigationItemRenderer', + 'escape_label' => false + ) + ) + ); + } +} + +if (($navigation->isEmpty() || ! $navigation->hasRenderableItems()) && $notes === '') { + return; +} +?> +<tr> + <th><?= $this->translate('Notes') ?></th> + <td> + <?= $navigation->getRenderer() ?> + <?php if ($notes !== ''): ?> + <?= $this->markdown($notes, [ + 'id' => $object->type . '-notes', + 'class' => 'collapsible', + 'data-visible-height' => 200 + ]) ?> + <?php endif ?> + </td> +</tr>
\ No newline at end of file diff --git a/modules/monitoring/application/views/scripts/show/components/notifications.phtml b/modules/monitoring/application/views/scripts/show/components/notifications.phtml new file mode 100644 index 0000000..3e8c665 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/notifications.phtml @@ -0,0 +1,68 @@ +<tr> + <th><?= $this->translate('Notifications') ?></th> + <td> + <?php + /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ + if ($this->hasPermission('monitoring/command/send-custom-notification')) { + if ($object->getType() === $object::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + echo $this->qlink( + $this->translate('Send notification'), + 'monitoring/host/send-custom-notification', + array('host' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'bell', + 'title' => $this->translate( + 'Send a custom notification to contacts responsible for this host' + ) + ) + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + echo $this->qlink( + $this->translate('Send notification'), + 'monitoring/service/send-custom-notification', + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), + array( + 'class' => 'action-link', + 'data-base-target' => '_self', + 'icon' => 'bell', + 'title' => $this->translate( + 'Send a custom notification to contacts responsible for this service' + ) + ) + ); + } + if (! in_array((int) $object->state, array(0, 99))) { + echo '<br>'; + } + } elseif (in_array((int) $object->state, array(0, 99))) { + echo '-'; + } + // We are not interested in notifications for OK or pending objects + if (! in_array((int) $object->state, array(0, 99))) { + if ($object->current_notification_number > 0) { + if ((int) $object->current_notification_number === 1) { + $msg = sprintf( + $this->translate('A notification has been sent for this issue %s.'), + $this->timeAgo($object->last_notification) + ); + } else { + $msg = sprintf( + $this->translate('%d notifications have been sent for this issue.'), + $object->current_notification_number + ) . '<br>' . sprintf( + $this->translate('The last one was sent %s.'), + $this->timeAgo($object->last_notification) + ); + } + } else { + $msg = $this->translate('No notification has been sent for this issue.'); + } + echo $msg; + } + ?> + </td> +</tr> diff --git a/modules/monitoring/application/views/scripts/show/components/output.phtml b/modules/monitoring/application/views/scripts/show/components/output.phtml new file mode 100644 index 0000000..34d8268 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/output.phtml @@ -0,0 +1,5 @@ +<h2><?= $this->translate('Plugin Output') ?></h2> +<div id="check-output-<?= $this->escape(str_replace(' ', '-', $object->check_command)) ?>" class="collapsible" data-visible-height="100"> + <?= $this->pluginOutput($object->output, false, $object->check_command) ?> + <?= $this->pluginOutput($object->long_output, false, $object->check_command) ?> +</div> diff --git a/modules/monitoring/application/views/scripts/show/components/perfdata.phtml b/modules/monitoring/application/views/scripts/show/components/perfdata.phtml new file mode 100644 index 0000000..78ea6d2 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/perfdata.phtml @@ -0,0 +1,4 @@ +<?php if ($object->perfdata): ?> +<h2><?= $this->translate('Performance data') ?></h2> +<div id="check-perfdata-<?= $this->escape(str_replace(' ', '-', $object->check_command)) ?>"><?= $this->perfdata($object->perfdata) ?></div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/show/components/reachable.phtml b/modules/monitoring/application/views/scripts/show/components/reachable.phtml new file mode 100644 index 0000000..8d55e84 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/reachable.phtml @@ -0,0 +1,15 @@ +<?php if ($object->is_reachable !== null): ?> +<tr> + <th> + <?= $this->translate('Reachable') ?> + </th> + <td> + <span class="check-source-meta"><?= (bool) $object->is_reachable ? $this->translate('yes') : $this->translate('no') ?></span> + <?php if ((bool) $object->is_reachable) { + echo $this->icon('circle', $this->translate('Is reachable'), array('class' => 'icon-stateful state-ok')); + } else { + echo $this->icon('circle', $this->translate('Not reachable'), array('class' => 'icon-stateful state-critical')); + } ?> + </td> +</tr> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/show/components/servicegroups.phtml b/modules/monitoring/application/views/scripts/show/components/servicegroups.phtml new file mode 100644 index 0000000..09ff248 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/servicegroups.phtml @@ -0,0 +1,20 @@ +<?php + +if (empty($object->servicegroups)) return; + +$list = array(); +foreach ($object->servicegroups as $name => $alias) { + $list[] = $this->qlink( + $alias, + 'monitoring/list/services', + array('servicegroup_name' => $name), + array('title' => sprintf($this->translate('List all services in the group "%s"'), $alias)) + ); +} + +printf( + "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n", + $this->translate('Servicegroups'), + implode(', ', $list) +); + diff --git a/modules/monitoring/application/views/scripts/show/components/status.phtml b/modules/monitoring/application/views/scripts/show/components/status.phtml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/components/status.phtml diff --git a/modules/monitoring/application/views/scripts/show/contact.phtml b/modules/monitoring/application/views/scripts/show/contact.phtml new file mode 100644 index 0000000..aa448ae --- /dev/null +++ b/modules/monitoring/application/views/scripts/show/contact.phtml @@ -0,0 +1,70 @@ +<?php $contactHelper = $this->getHelper('ContactFlags') ?> +<div class="controls"> + <?php if (! $this->compact): ?> + <?= $this->tabs; ?> + <?php endif ?> + <h1><?= $this->translate('Contact details') ?></h1> + <div class="circular" style="background-image: url('<?= + $this->href('static/gravatar', array('email' => $contact->contact_email)) + ?>';width:120px;height:120px;)"></div> + +<?php if (! $contact): ?> + <?= $this->translate('No such contact') ?>: <?= $contactName ?> +</div> +<?php return; endif ?> + + <table class="name-value-table"> + <tbody> + <tr> + <th style="width: 20%"></th> + <td><strong><?= $this->escape($contact->contact_alias) ?></strong> (<?= $contact->contact_name ?>)</td> + </tr> +<?php if ($contact->contact_email): ?> + <tr> + <th><?= $this->translate('Email') ?></th> + <td> + <a href="mailto:<?= $contact->contact_email; ?>" title="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias); ?>" aria-label="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias); ?>"> + <?= $this->escape($contact->contact_email); ?> + </a> + </td> + </tr> +<?php endif ?> +<?php if ($contact->contact_pager): ?> + <tr> + <th><?= $this->translate('Pager') ?></th> + <td><?= $this->escape($contact->contact_pager) ?></td> + </tr> +<?php endif ?> + <tr> + <th><?= $this->translate('Hosts') ?></th> + <td><?= $this->escape($contactHelper->contactFlags($contact, 'host')) ?><br /> + <?= $this->escape($contact->contact_notify_host_timeperiod) ?></td> + </tr> + <tr> + <th><?= $this->translate('Services') ?></th> + <td><?= $this->escape($contactHelper->contactFlags($contact, 'service')) ?><br /> + <?= $this->escape($contact->contact_notify_service_timeperiod) ?></td> + </tr> + </tbody> + </table> + <?php if (count($commands)): ?> + <h1><?= $this->translate('Commands') ?>:</h1> + <ul> + <?php foreach ($commands as $command): ?> + <li><?= $command->command_name ?></li> + <?php endforeach ?> + </ul> + <?php endif ?> + <h1><?= $this->translate('Notifications sent to this contact') ?></h1> + <?= $this->limiter; ?> + <?= $this->paginator; ?> +</div> + +<?php if (count($notifications)): ?> +<?= $this->partial('list/notifications.phtml', array( + 'notifications' => $notifications, + 'compact' => true +)); ?> +<?php else: ?> +<div class="content"><?= $this->translate('No notifications have been sent for this contact') ?></div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml b/modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml new file mode 100644 index 0000000..e6dc0be --- /dev/null +++ b/modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml @@ -0,0 +1,131 @@ +<div class="box hostservicechecks col-1-2"> + <div class="box header"> + <h2><?= $this->translate('Host and Service Checks'); ?></h2> + </div> + <div class="box contents"> + <table> + <thead> + <tr> + <th><?= $this->translate('Hosts'); ?></th> + <th><?= $this->translate('Services'); ?></th> + </tr> + </thead> + <tbody> + <tr> + <td> +<?php if ($this->statusSummary->hosts_active): ?> + <div class="box entry"><?= $this->qlink( + sprintf( + $this->translatePlural('%u Active', '%u Active', $this->statusSummary->hosts_active), + $this->statusSummary->hosts_active + ), + 'monitoring/list/hosts', + array('host_active_checks_enabled' => 1), + array('title' => sprintf( + $this->translatePlural( + 'List %u actively checked host', + 'List %u actively checked hosts', + $this->statusSummary->hosts_active + ), + $this->statusSummary->hosts_active + )) + ); ?></div> +<?php endif ?> +<?php if ($this->statusSummary->hosts_passive): ?> + <div class="box entry"><?= $this->qlink( + sprintf( + $this->translatePlural('%d Passive', '%d Passive', $this->statusSummary->hosts_passive), + $this->statusSummary->hosts_passive + ), + 'monitoring/list/hosts', + array('host_active_checks_enabled' => 0, 'host_passive_checks_enabled' => 1), + array('title' => sprintf( + $this->translatePlural( + 'List %u passively checked host', + 'List %u passively checked hosts', + $this->statusSummary->hosts_passive + ), + $this->statusSummary->hosts_passive + )) + ); ?></div> +<?php endif ?> +<?php if ($this->statusSummary->hosts_not_checked): ?> + <div class="box entry"><?= $this->qlink( + sprintf( + $this->translatePlural('%d Disabled', '%d Disabled', $this->statusSummary->hosts_not_checked), + $this->statusSummary->hosts_not_checked + ), + 'monitoring/list/hosts', + array('host_active_checks_enabled' => 0, 'host_passive_checks_enabled' => 0), + array('title' => sprintf( + $this->translatePlural( + 'List %u host that is not being checked at all', + 'List %u hosts which are not being checked at all', + $this->statusSummary->hosts_not_checked + ), + $this->statusSummary->hosts_not_checked + )) + ); ?></div> +<?php endif ?> + </td> + <td> +<?php if ($this->statusSummary->services_active): ?> + <div class="box entry"><?= $this->qlink( + sprintf( + $this->translatePlural('%d Active', '%d Active', $this->statusSummary->services_active), + $this->statusSummary->services_active + ), + 'monitoring/list/services', + array('service_active_checks_enabled' => 1), + array('title' => sprintf( + $this->translatePlural( + 'List %u actively checked service', + 'List %u actively checked services', + $this->statusSummary->services_active + ), + $this->statusSummary->services_active + )) + ); ?></div> +<?php endif ?> +<?php if ($this->statusSummary->services_passive): ?> + <div class="box entry"><?= $this->qlink( + sprintf( + $this->translatePlural('%d Passive', '%d Passive', $this->statusSummary->services_passive), + $this->statusSummary->services_passive + ), + 'monitoring/list/services', + array('service_active_checks_enabled' => 0, 'service_passive_checks_enabled' => 1), + array('title' => sprintf( + $this->translatePlural( + 'List %u passively checked service', + 'List %u passively checked services', + $this->statusSummary->services_passive + ), + $this->statusSummary->services_passive + )) + ); ?></div> +<?php endif ?> +<?php if ($this->statusSummary->services_not_checked): ?> + <div class="box entry"><?= $this->qlink( + sprintf( + $this->translatePlural('%d Disabled', '%d Disabled', $this->statusSummary->services_not_checked), + $this->statusSummary->services_not_checked + ), + 'monitoring/list/services', + array('service_active_checks_enabled' => 0, 'service_passive_checks_enabled' => 0), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is not being checked at all', + 'List %u services which are not being checked at all', + $this->statusSummary->services_not_checked + ), + $this->statusSummary->services_not_checked + )) + ); ?></div> +<?php endif ?> + </td> + </tr> + </tbody> + </table> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml b/modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml new file mode 100644 index 0000000..eeeec16 --- /dev/null +++ b/modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml @@ -0,0 +1,287 @@ +<div class="box monitoringfeatures col-1-2"> + <div class="box header"> + <h2><?= $this->translate('Monitoring Features'); ?></h2> + </div> + <div class="box contents"> +<?php if ($this->statusSummary->hosts_without_flap_detection || $this->statusSummary->services_without_flap_detection || + $this->statusSummary->hosts_flapping || $this->statusSummary->services_flapping): ?> + <div class="box-separator badge feature-highlight"><?= $this->translate('Flap Detection'); ?></div> +<?php else: ?> + <div class="box-separator badge"><?= $this->translate('Flap Detection'); ?></div> +<?php endif ?> + <table> + <tbody> + <tr> + <td> + <div class="box entry"> +<?php if ($this->statusSummary->hosts_without_flap_detection): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Host Disabled', '%u Hosts Disabled', $this->statusSummary->hosts_without_flap_detection), + $this->statusSummary->hosts_without_flap_detection + ), + 'monitoring/list/hosts', + array('host_flap_detection_enabled' => 0), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u host for which flap detection has been disabled', + 'List %u hosts for which flap detection has been disabled', + $this->statusSummary->hosts_without_flap_detection + ), + $this->statusSummary->hosts_without_flap_detection + ) + ) + ); ?> +<?php else: ?> + <?= $this->qlink( + $this->translate('All Hosts Enabled'), + 'monitoring/list/hosts', + array('host_flap_detection_enabled' => 1), + array('title' => $this->translate( + 'List all hosts, for which flap detection is enabled entirely' + )) + ); ?> +<?php endif ?> +<?php if ($this->statusSummary->hosts_flapping): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Host Flapping', '%u Hosts Flapping', $this->statusSummary->hosts_flapping), + $this->statusSummary->hosts_flapping + ), + 'monitoring/list/hosts', + array('host_is_flapping' => 1), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is currently flapping', + 'List %u hosts which are currently flapping', + $this->statusSummary->hosts_flapping + ), + $this->statusSummary->hosts_flapping + ) + ) + ); ?> +<?php endif ?> + </div> + </td> + <td> + <div class="box entry"> +<?php if ($this->statusSummary->services_without_flap_detection): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Service Disabled', '%u Services Disabled', $this->statusSummary->services_without_flap_detection), + $this->statusSummary->services_without_flap_detection + ), + 'monitoring/list/services', + array('service_flap_detection_enabled' => 0), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u service for which flap detection has been disabled', + 'List %u services for which flap detection has been disabled', + $this->statusSummary->services_without_flap_detection + ), + $this->statusSummary->services_without_flap_detection + ) + ) + ); ?> +<?php else: ?> + <?= $this->qlink( + $this->translate('All Services Enabled'), + 'monitoring/list/services', + array('service_flap_detection_enabled' => 1), + array('title' => $this->translate( + 'List all services, for which flap detection is enabled entirely' + )) + ); ?> +<?php endif ?> +<?php if ($this->statusSummary->services_flapping): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Service Flapping', '%u Services Flapping', $this->statusSummary->services_flapping), + $this->statusSummary->services_flapping + ), + 'monitoring/list/services', + array('service_is_flapping' => 1), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u service that is currently flapping', + 'List %u services which are currently flapping', + $this->statusSummary->services_flapping + ), + $this->statusSummary->services_flapping + ) + ) + ); ?> +<?php endif ?> + </div> + </td> + </tr> + </tbody> + </table> +<?php if ($this->statusSummary->hosts_not_triggering_notifications || $this->statusSummary->services_not_triggering_notifications): ?> + <div class="box-separator badge feature-highlight"><?= $this->translate('Notifications'); ?></div> +<?php else: ?> + <div class="box-separator badge"><?= $this->translate('Notifications'); ?></div> +<?php endif ?> + <table> + <tbody> + <tr> + <td> + <div class="box entry"> +<?php if ($this->statusSummary->hosts_not_triggering_notifications): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Host Disabled', '%u Hosts Disabled', $this->statusSummary->hosts_not_triggering_notifications), + $this->statusSummary->hosts_not_triggering_notifications + ), + 'monitoring/list/hosts', + array('host_notifications_enabled' => 0), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u host for which notifications are suppressed', + 'List %u hosts for which notifications are suppressed', + $this->statusSummary->hosts_not_triggering_notifications + ), + $this->statusSummary->hosts_not_triggering_notifications + ) + ) + ); ?> +<?php else: ?> + <?= $this->qlink( + $this->translate('All Hosts Enabled'), + 'monitoring/list/hosts', + array('host_notifications_enabled' => 1), + array('title' => $this->translate( + 'List all hosts, for which notifications are enabled entirely' + )) + ); ?> +<?php endif ?> + </div> + </td> + <td> + <div class="box entry"> +<?php if ($this->statusSummary->services_not_triggering_notifications): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Service Disabled', '%u Services Disabled', $this->statusSummary->services_not_triggering_notifications), + $this->statusSummary->services_not_triggering_notifications + ), + 'monitoring/list/services', + array('service_notifications_enabled' => 0), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u service for which notifications are suppressed', + 'List %u services for which notifications are suppressed', + $this->statusSummary->services_not_triggering_notifications + ), + $this->statusSummary->services_not_triggering_notifications + ) + ) + ); ?> +<?php else: ?> + <?= $this->qlink( + $this->translate('All Services Enabled'), + 'monitoring/list/services', + array('service_notifications_enabled' => 1), + array('title' => $this->translate( + 'List all services, for which notifications are enabled entirely' + )) + ); ?> +<?php endif ?> + </div> + </td> + </tr> + </tbody> + </table> +<?php if ($this->statusSummary->hosts_not_processing_event_handlers || $this->statusSummary->services_not_processing_event_handlers): ?> + <div class="box-separator badge feature-highlight"><?= $this->translate('Event Handlers'); ?></div> +<?php else: ?> + <div class="box-separator badge"><?= $this->translate('Event Handlers'); ?></div> +<?php endif ?> + <table> + <tbody> + <tr> + <td> + <div class="box entry"> +<?php if ($this->statusSummary->hosts_not_processing_event_handlers): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Host Disabled', '%u Hosts Disabled', $this->statusSummary->hosts_not_processing_event_handlers), + $this->statusSummary->hosts_not_processing_event_handlers + ), + 'monitoring/list/hosts', + array('host_event_handler_enabled' => 0), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u host that is not processing any event handlers', + 'List %u hosts which are not processing any event handlers', + $this->statusSummary->hosts_not_processing_event_handlers + ), + $this->statusSummary->hosts_not_processing_event_handlers + ) + ) + ); ?> +<?php else: ?> + <?= $this->qlink( + $this->translate('All Hosts Enabled'), + 'monitoring/list/hosts', + array('host_event_handler_enabled' => 1), + array('title' => $this->translate( + 'List all hosts, which are processing event handlers entirely' + )) + ); ?> +<?php endif ?> + </div> + </td> + <td> + <div class="box entry"> +<?php if ($this->statusSummary->services_not_processing_event_handlers): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural('%u Service Disabled', '%u Services Disabled', $this->statusSummary->services_not_processing_event_handlers), + $this->statusSummary->services_not_processing_event_handlers + ), + 'monitoring/list/services', + array('service_event_handler_enabled' => 0), + array( + 'class' => 'feature-highlight', + 'title' => sprintf( + $this->translatePlural( + 'List %u service that is not processing any event handlers', + 'List %u services which are not processing any event handlers', + $this->statusSummary->services_not_processing_event_handlers + ), + $this->statusSummary->services_not_processing_event_handlers + ) + ) + ); ?> +<?php else: ?> + <?= $this->qlink( + $this->translate('All Services Enabled'), + 'monitoring/list/services', + array('service_event_handler_enabled' => 1), + array('title' => $this->translate( + 'List all services, which are processing event handlers entirely' + )) + ); ?> +<?php endif ?> + </div> + </td> + </tr> + </tbody> + </table> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml b/modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml new file mode 100644 index 0000000..05ffd29 --- /dev/null +++ b/modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml @@ -0,0 +1,81 @@ +<?php +$service_problems = ( + $this->statusSummary->services_warning_handled_on_ok_hosts || + $this->statusSummary->services_warning_unhandled_on_ok_hosts || + $this->statusSummary->services_critical_handled_on_ok_hosts || + $this->statusSummary->services_critical_unhandled_on_ok_hosts || + $this->statusSummary->services_unknown_handled_on_ok_hosts || + $this->statusSummary->services_unknown_unhandled_on_ok_hosts +); +?> +<div class="box ok_hosts state_<?= $this->statusSummary->hosts_up ? 'up' : 'pending'; ?> col-1-2"> + <div class="box header"> + <?php if ($this->statusSummary->hosts_up): ?> + <h2><?= $this->qlink( + sprintf( + $this->translatePlural('%u Host UP', '%u Hosts UP', $this->statusSummary->hosts_up), + $this->statusSummary->hosts_up + ), + 'monitoring/list/hosts', + array('host_state' => 0), + array('title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state UP', + 'List %u hosts which are currently in state UP', + $this->statusSummary->hosts_up + ), + $this->statusSummary->hosts_up + )) + ); ?></h2> + <?php endif ?> + <?php if ($this->statusSummary->hosts_pending): ?> + <h2><?= $this->qlink( + sprintf( + $this->translatePlural('%u Host PENDING', '%u Hosts PENDING', $this->statusSummary->hosts_pending), + $this->statusSummary->hosts_pending + ), + 'monitoring/list/hosts', + array('host_state' => 99), + array('title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state PENDING', + 'List %u hosts which are currently in state PENDING', + $this->statusSummary->hosts_pending + ), + $this->statusSummary->hosts_pending + )) + ); ?></h2> + <?php endif ?> + </div> +<?php if ($service_problems || $this->statusSummary->hosts_down || $this->statusSummary->hosts_unreachable): ?> + <div class="box contents"> + <?= $this->partial( + 'tactical/components/parts/servicestatesummarybyhoststate.phtml', + array( + 'translationDomain' => $this->translationDomain, + 'host_problem' => 0, + 'services_ok' => $this->statusSummary->services_ok_on_ok_hosts, + 'services_ok_not_checked' => $this->statusSummary->services_ok_not_checked_on_ok_hosts, + 'services_pending' => $this->statusSummary->services_pending_on_ok_hosts, + 'services_pending_not_checked' => $this->statusSummary->services_pending_not_checked_on_ok_hosts, + 'services_warning_handled' => $this->statusSummary->services_warning_handled_on_ok_hosts, + 'services_warning_unhandled' => $this->statusSummary->services_warning_unhandled_on_ok_hosts, + 'services_warning_passive' => $this->statusSummary->services_warning_passive_on_ok_hosts, + 'services_warning_not_checked' => $this->statusSummary->services_warning_not_checked_on_ok_hosts, + 'services_critical_handled' => $this->statusSummary->services_critical_handled_on_ok_hosts, + 'services_critical_unhandled' => $this->statusSummary->services_critical_unhandled_on_ok_hosts, + 'services_critical_passive' => $this->statusSummary->services_critical_passive_on_ok_hosts, + 'services_critical_not_checked' => $this->statusSummary->services_critical_not_checked_on_ok_hosts, + 'services_unknown_handled' => $this->statusSummary->services_unknown_handled_on_ok_hosts, + 'services_unknown_unhandled' => $this->statusSummary->services_unknown_unhandled_on_ok_hosts, + 'services_unknown_passive' => $this->statusSummary->services_unknown_passive_on_ok_hosts, + 'services_unknown_not_checked' => $this->statusSummary->services_unknown_not_checked_on_ok_hosts + ) + ); ?> +<?php else: ?> + <div class="box contents zero"> + <h3>0</h3> + <span><?= $this->translate('Service Problems'); ?></span> +<?php endif ?> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml b/modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml new file mode 100644 index 0000000..4f32daf --- /dev/null +++ b/modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml @@ -0,0 +1,394 @@ +<?php + +use Icinga\Module\Monitoring\Object\Service; + +?> +<?php if ($services_critical_handled || $services_critical_unhandled): ?> +<div class="box badge entry state-<?= Service::getStateText(2); ?> <?= $services_critical_unhandled ? '' : 'handled'; ?>"> +<?php if ($services_critical_unhandled): ?> + <?= $this->qlink( + $services_critical_unhandled . ' ' . Service::getStateText(2, true), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 2, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state CRITICAL', + 'List %u services which are currently in state CRITICAL', + $services_critical_unhandled + ), + $services_critical_unhandled + )) + ); ?> +<?php endif ?> +<?php if ($services_critical_handled): ?> + <?= $this->qlink( + $services_critical_handled . ' ' . ( + $services_critical_unhandled ? $this->translate('Handled') : Service::getStateText(2, true) + ), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 2, + 'service_handled' => 1 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state CRITICAL (Handled)', + 'List %u services which are currently in state CRITICAL (Handled)', + $services_critical_handled + ), + $services_critical_handled + )) + ); ?> +<?php endif ?> +<?php if ($services_critical_passive): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is passively checked', + '%u are passively checked', + $services_critical_passive + ), + $services_critical_passive + ), + 'monitoring/list/services', + array( + 'service_state' => 2, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 1 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state CRITICAL and passively checked', + 'List %u services which are currently in state CRITICAL and passively checked', + $services_critical_passive + ), + $services_critical_passive + )) + ); ?> +<?php endif ?> +<?php if ($services_critical_not_checked): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is not checked at all', + '%u are not checked at all', + $services_critical_not_checked + ), + $services_critical_not_checked + ), + 'monitoring/list/services', + array( + 'service_state' => 2, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state CRITICAL and not checked at all', + 'List %u services which are currently in state CRITICAL and not checked at all', + $services_critical_not_checked + ), + $services_critical_not_checked + )) + ); ?> +<?php endif ?> +</div> +<?php endif ?> +<?php if ($services_warning_handled || $services_warning_unhandled): ?> +<div class="box badge entry state-<?= Service::getStateText(1); ?> <?= $services_warning_unhandled ? '' : 'handled'; ?>"> +<?php if ($services_warning_unhandled): ?> + <?= $this->qlink( + $services_warning_unhandled . ' ' . Service::getStateText(1, true), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 1, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state WARNING', + 'List %u services which are currently in state WARNING', + $services_warning_unhandled + ), + $services_warning_unhandled + )) + ); ?> +<?php endif ?> +<?php if ($services_warning_handled): ?> + <?= $this->qlink( + $services_warning_handled . ' ' . ( + $services_warning_unhandled ? $this->translate('Handled') : Service::getStateText(1, true) + ), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 1, + 'service_handled' => 1 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state WARNING (Handled)', + 'List %u services which are currently in state WARNING (Handled)', + $services_warning_handled + ), + $services_warning_handled + )) + ); ?> +<?php endif ?> +<?php if ($services_warning_passive): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is passively checked', + '%u are passively checked', + $services_warning_passive + ), + $services_warning_passive + ), + 'monitoring/list/services', + array( + 'service_state' => 1, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 1 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state WARNING and passively checked', + 'List %u services which are currently in state WARNING and passively checked', + $services_warning_passive + ), + $services_warning_passive + )) + ); ?> +<?php endif ?> +<?php if ($services_warning_not_checked): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is not checked at all', + '%u are not checked at all', + $services_warning_not_checked + ), + $services_warning_not_checked + ), + 'monitoring/list/services', + array( + 'service_state' => 1, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state WARNING and not checked at all', + 'List %u services which are currently in state WARNING and not checked at all', + $services_warning_not_checked + ), + $services_warning_not_checked + )) + ); ?> +<?php endif ?> +</div> +<?php endif ?> +<?php if ($services_unknown_handled || $services_unknown_unhandled): ?> +<div class="box badge entry state-<?= Service::getStateText(3); ?> <?= $services_unknown_unhandled ? '' : 'handled'; ?>"> +<?php if ($services_unknown_unhandled): ?> + <?= $this->qlink( + $services_unknown_unhandled . ' ' . Service::getStateText(3, true), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 3, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state UNKNOWN', + 'List %u services which are currently in state UNKNOWN', + $services_unknown_unhandled + ), + $services_unknown_unhandled + )) + ); ?> +<?php endif ?> +<?php if ($services_unknown_handled): ?> + <?= $this->qlink( + $services_unknown_handled . ' ' . ( + $services_unknown_unhandled ? $this->translate('Handled') : Service::getStateText(3, true) + ), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 3, + 'service_handled' => 1 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state UNKNOWN (Handled)', + 'List %u services which are currently in state UNKNOWN (Handled)', + $services_unknown_handled + ), + $services_unknown_handled + )) + ); ?> +<?php endif ?> +<?php if ($services_unknown_passive): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is passively checked', + '%u are passively checked', + $services_unknown_passive + ), + $services_unknown_passive + ), + 'monitoring/list/services', + array( + 'service_state' => 3, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 1 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state UNKNOWN and passively checked', + 'List %u services which are currently in state UNKNOWN and passively checked', + $services_unknown_passive + ), + $services_unknown_passive + )) + ); ?> +<?php endif ?> +<?php if ($services_unknown_not_checked): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is not checked at all', + '%u are not checked at all', + $services_unknown_not_checked + ), + $services_unknown_not_checked + ), + 'monitoring/list/services', + array( + 'service_state' => 3, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state UNKNOWN and not checked at all', + 'List %u services which are currently in state UNKNOWN and not checked at all', + $services_unknown_not_checked + ), + $services_unknown_not_checked + )) + ); ?> +<?php endif ?> +</div> +<?php endif ?> +<?php if ($services_ok): ?> +<div class="box badge entry state-<?= Service::getStateText(0); ?>"> + <?= $this->qlink( + $services_ok . ' ' . Service::getStateText(0, true), + 'monitoring/list/services', + array( + 'host_problem' => $host_problem, + 'service_state' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state OK', + 'List %u services which are currently in state OK', + $services_ok + ), + $services_ok + )) + ); ?> +<?php if ($services_ok_not_checked): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is not checked at all', + '%u are not checked at all', + $services_ok_not_checked + ), + $services_ok_not_checked + ), + 'monitoring/list/services', + array( + 'service_state' => 0, + 'host_problem' => $host_problem, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state OK and not checked at all', + 'List %u services which are currently in state OK and not checked at all', + $services_ok_not_checked + ), + $services_ok_not_checked + )) + ); ?> +<?php endif ?> +</div> +<?php endif ?> +<?php if ($services_pending): ?> +<div class="box badge entry state-<?= Service::getStateText(99); ?>"> + <?= $this->qlink( + $services_pending . ' ' . Service::getStateText(99, true), + 'monitoring/list/services', + array( + 'service_state' => 99 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state PENDING', + 'List %u services which are currently in state PENDING', + $services_pending + ), + $services_pending + )) + ); ?> +<?php if ($services_pending_not_checked): ?> + <?= $this->qlink( + sprintf( + $this->translatePlural( + '%u is not checked at all', + '%u are not checked at all', + $services_pending_not_checked + ), + $services_pending_not_checked + ), + 'monitoring/list/services', + array( + 'service_state' => 99, + 'service_active_checks_enabled' => 0, + 'service_passive_checks_enabled' => 0 + ), + array('title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state PENDING and not checked at all', + 'List %u services which are currently in state PENDING and not checked at all', + $services_pending_not_checked + ), + $services_pending_not_checked + )) + ); ?> +<?php endif ?> +</div> +<?php endif ?> diff --git a/modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml b/modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml new file mode 100644 index 0000000..6374ff8 --- /dev/null +++ b/modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml @@ -0,0 +1,74 @@ +<div class="box problem_hosts <?php + echo $this->statusSummary->hosts_down ? 'state_down' : 'state_unreachable'; + if (!$this->statusSummary->hosts_down_unhandled && !$this->statusSummary->hosts_unreachable_unhandled) { + echo ' handled'; + } +?> col-1-2"> + <div class="box header"> + <?php if ($this->statusSummary->hosts_down): ?> + <h2><?= $this->qlink( + sprintf( + $this->translatePlural('%u Host DOWN', '%u Hosts DOWN', $this->statusSummary->hosts_down), + $this->statusSummary->hosts_down + ), + 'monitoring/list/hosts', + array('host_state' => 1), + array('title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state DOWN', + 'List %u hosts which are currently in state DOWN', + $this->statusSummary->hosts_down + ), + $this->statusSummary->hosts_down + )) + ); ?></h2> + <?php endif ?> + <?php if ($this->statusSummary->hosts_unreachable): ?> + <h2><?= $this->qlink( + sprintf( + $this->translatePlural( + '%u Host UNREACHABLE', + '%u Hosts UNREACHABLE', + $this->statusSummary->hosts_unreachable + ), + $this->statusSummary->hosts_unreachable + ), + 'monitoring/list/hosts', + array('host_state' => 2), + array('title' => sprintf( + $this->translatePlural( + 'List %u host that is currently in state UNREACHABLE', + 'List %u hosts which are currently in state UNREACHABLE', + $this->statusSummary->hosts_unreachable + ), + $this->statusSummary->hosts_unreachable + )) + ); ?></h2> + <?php endif ?> + </div> + <div class="box contents"> + <?= $this->partial( + 'tactical/components/parts/servicestatesummarybyhoststate.phtml', + array( + 'translationDomain' => $this->translationDomain, + 'host_problem' => 1, + 'services_ok' => $this->statusSummary->services_ok_on_problem_hosts, + 'services_ok_not_checked' => $this->statusSummary->services_ok_not_checked_on_problem_hosts, + 'services_pending' => $this->statusSummary->services_pending_on_problem_hosts, + 'services_pending_not_checked' => $this->statusSummary->services_pending_not_checked_on_problem_hosts, + 'services_warning_handled' => $this->statusSummary->services_warning_handled_on_problem_hosts, + 'services_warning_unhandled' => $this->statusSummary->services_warning_unhandled_on_problem_hosts, + 'services_warning_passive' => $this->statusSummary->services_warning_passive_on_problem_hosts, + 'services_warning_not_checked' => $this->statusSummary->services_warning_not_checked_on_problem_hosts, + 'services_critical_handled' => $this->statusSummary->services_critical_handled_on_problem_hosts, + 'services_critical_unhandled' => $this->statusSummary->services_critical_unhandled_on_problem_hosts, + 'services_critical_passive' => $this->statusSummary->services_critical_passive_on_problem_hosts, + 'services_critical_not_checked' => $this->statusSummary->services_critical_not_checked_on_problem_hosts, + 'services_unknown_handled' => $this->statusSummary->services_unknown_handled_on_problem_hosts, + 'services_unknown_unhandled' => $this->statusSummary->services_unknown_unhandled_on_problem_hosts, + 'services_unknown_passive' => $this->statusSummary->services_unknown_passive_on_problem_hosts, + 'services_unknown_not_checked' => $this->statusSummary->services_unknown_not_checked_on_problem_hosts + ) + ); ?> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/tactical/index.phtml b/modules/monitoring/application/views/scripts/tactical/index.phtml new file mode 100644 index 0000000..12f4bc5 --- /dev/null +++ b/modules/monitoring/application/views/scripts/tactical/index.phtml @@ -0,0 +1,145 @@ +<?php +use Icinga\Data\Filter\Filter; +?> +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?= $this->filterEditor ?> +</div> +<?php endif ?> +<div class="content tactical grid"> +<?php if (! count(array_filter((array) $statusSummary))): ?> + <p><?= $this->translate('No results found matching the filter.') ?></p> +</div> +<?php return; endif ?> + <div class="boxview" data-base-target="_next"> + <div class="donut-container"> + <h2 aria-label="<?= $this->translate('Host Summary') ?>"><?= $this->translate('Host Summary') ?></h2> + <div class="donut"> + <?= $hostStatusSummaryChart ?> + </div> + <ul class="donut-legend"> + <?php if ($statusSummary->hosts_up): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-ok badge"><?= $statusSummary->hosts_up ?></span><?= $this->translate('Up') ?> + </a> + </li> + <?php endif ?> + <?php if ($statusSummary->hosts_down_handled): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 1, 'host_handled' => 1, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-critical handled badge"><?= $statusSummary->hosts_down_handled ?></span><?= $this->translate('Down') ?> (<?= $this->translate('Handled') ?>) + </a> + </li> + <?php endif ?> + <?php if ($statusSummary->hosts_down_unhandled): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 1, 'host_handled' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-critical badge"><?= $statusSummary->hosts_down_unhandled ?></span><?= $this->translate('Down') ?> + </a> + </li> + <?php endif ?> + <?php if ($statusSummary->hosts_unreachable_handled): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 2, 'host_handled' => 1, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-unreachable handled badge"><?= $statusSummary->hosts_unreachable_handled ?></span><?= $this->translate('Unreachable') ?> (<?= $this->translate('Handled') ?>) + </a> + </li> + <?php endif ?> + <?php if ($statusSummary->hosts_unreachable_unhandled): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 2, 'host_handled' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-unreachable badge"><?= $statusSummary->hosts_unreachable_unhandled ?></span><?= $this->translate('Unreachable') ?> + </a> + </li> + <?php endif ?> + <?php if ($statusSummary->hosts_pending): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 99, 'sort' => 'host_last_check', 'dir' => 'asc'))->addFilter(Filter::not(Filter::where('host_active_checks_enabled', 0), Filter::where('host_passive_checks_enabled', 0))) ?>"> + <span class="state state-pending badge"><?= $statusSummary->hosts_pending ?></span><?= $this->translate('Pending') ?> + </a> + </li> + <?php endif ?> + <?php if ($statusSummary->hosts_pending_not_checked): ?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 99, 'host_active_checks_enabled' => 0, 'host_passive_checks_enabled' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>"> + <span class="state slice-state-not-checked badge"><?= $statusSummary->hosts_pending_not_checked ?></span><?= $this->translate('Not Checked') ?> + </a> + </li> + <?php endif ?> + </ul> + </div> + <div class="donut-container"> + <h2 aria-label="<?= $this->translate('Service Summary') ?>"><?= $this->translate('Service Summary') ?></h2> + <div class="donut"> + <?= $serviceStatusSummaryChart ?> + </div> + <ul class="donut-legend"> + <?php if ($statusSummary->services_ok):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-ok badge"><?= $statusSummary->services_ok ?></span><?= $this->translate('Ok') ?> + </a> + </li> + <?php endif; + if ($statusSummary->services_warning_handled):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 1, 'service_handled' => 1, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-warning handled badge"><?= $statusSummary->services_warning_handled ?></span><?= $this->translate('Warning') ?> (<?= $this->translate('Handled') ?>) + </a> + </li> + <?php endif; + if ($statusSummary->services_warning_unhandled):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 1, 'service_handled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-warning badge"><?= $statusSummary->services_warning_unhandled ?></span><?= $this->translate('Warning') ?> + </a> + </li> + <?php endif; + if ($statusSummary->services_critical_handled):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 2, 'service_handled' => 1, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-critical handled badge"><?= $statusSummary->services_critical_handled ?></span><?= $this->translate('Critical') ?> (<?= $this->translate('Handled') ?>) + </a> + </li> + <?php endif; + if ($statusSummary->services_critical_unhandled):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 2, 'service_handled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-critical badge"><?= $statusSummary->services_critical_unhandled ?></span><?= $this->translate('Critical') ?> + </a> + </li> + <?php endif; + if ($statusSummary->services_unknown_handled):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 3, 'service_handled' => 1, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-unknown handled badge"><?= $statusSummary->services_unknown_handled ?></span><?= $this->translate('Unknown') ?> (<?= $this->translate('Handled') ?>) + </a> + </li> + <?php endif; + if ($statusSummary->services_unknown_unhandled):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 3, 'service_handled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state state-unknown badge"><?= $statusSummary->services_unknown_unhandled ?></span><?= $this->translate('Unknown') ?> + </a> + </li> + <?php endif; + if ($statusSummary->services_pending):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 99, 'sort' => 'service_last_check', 'dir' => 'asc'))->addFilter(Filter::not(Filter::where('service_active_checks_enabled', 0), Filter::where('service_passive_checks_enabled', 0))) ?>"> + <span class="state state-pending badge"><?= $statusSummary->services_pending ?></span><?= $this->translate('Pending') ?> + </a> + </li> + <?php endif; + if ($statusSummary->services_pending_not_checked):?> + <li> + <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 99, 'service_active_checks_enabled' => 0, 'service_passive_checks_enabled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>"> + <span class="state slice-state-not-checked badge"><?= $statusSummary->services_pending_not_checked ?></span><?= $this->translate('Not Checked') ?> + </a> + </li> + <?php endif?> + </ul> + </div> + </div> +</div> diff --git a/modules/monitoring/application/views/scripts/timeline/index.phtml b/modules/monitoring/application/views/scripts/timeline/index.phtml new file mode 100644 index 0000000..af3b406 --- /dev/null +++ b/modules/monitoring/application/views/scripts/timeline/index.phtml @@ -0,0 +1,145 @@ +<?php +use Icinga\Web\Url; +use Icinga\Util\Color; + +$groupInfo = $timeline->getGroupInfo(); +$firstRow = ! $beingExtended; + +if (! $beingExtended && !$this->compact): ?> +<div class="controls"> + <?= $this->tabs; ?> + <div class="dontprint"> + <?= $intervalBox; ?> + </div> + <div class="timeline-legend"> + <h2><?= $this->translate('Legend'); ?></h2> +<?php foreach ($groupInfo as $labelAndClass): ?> + <span class="<?= $labelAndClass['class'] ?>"> + <span><?= $labelAndClass['label']; ?></span> + </span> +<?php endforeach ?> + </div> +</div> +<?php endif ?> +<?php if (! $beingExtended): ?> +<div class="content" data-base-target="_next"> + <div class="timeline"> +<?php endif ?> +<?php if ($switchedContext): ?> + <hr> +<?php endif ?> +<?php foreach ($timeline as $timeInfo): + switch ($intervalBox->getInterval()) { + case '1d': + $titleTime = sprintf( + $this->translate('on %s', 'timeline.link.title.time'), + $timeInfo[0]->end->format('d/m/Y') + ); + break; + case '1w': + $titleTime = sprintf( + $this->translate('in week %s of %s', 'timeline.link.title.week.and.year'), + $timeInfo[0]->end->format('W'), + $timeInfo[0]->end->format('Y') + ); + break; + case '1m': + $titleTime = sprintf( + $this->translate('in %s', 'timeline.link.title.month.and.year'), + $timeInfo[0]->end->format('F Y') + ); + break; + case '1y': + $titleTime = sprintf( + $this->translate('in %s', 'timeline.link.title.year'), + $timeInfo[0]->end->format('Y') + ); + break; + default: + $titleTime = sprintf( + $this->translate('between %s and %s', 'timeline.link.title.datetime.twice'), + $timeInfo[0]->end->format('d/m/Y g:i A'), + $timeInfo[0]->start->format('d/m/Y g:i A') + ); + } ?> + <div class="timeframe"> + <span><?= $this->qlink( + $timeInfo[0]->end->format($intervalFormat), + 'monitoring/list/eventhistory', + array( + 'timestamp<' => $timeInfo[0]->start->getTimestamp(), + 'timestamp>' => $timeInfo[0]->end->getTimestamp() + ), + array('title' => sprintf( + $this->translate('List all event records registered %s', 'timeline.link.title'), + $titleTime + )), + false + ); ?></span> +<?php foreach ($groupInfo as $groupName => $labelAndColor): ?> +<?php if (array_key_exists($groupName, $timeInfo[1])): ?> +<?php +$circleWidth = $timeline->calculateCircleWidth($timeInfo[1][$groupName], 2); +$extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$groupName], 2); +?> +<?php if ($firstRow && $extrapolatedCircleWidth !== $circleWidth): ?> + <div class="circle-box" style="width: <?= $extrapolatedCircleWidth; ?>;"> + <div class="outer-circle extrapolated <?= $timeInfo[1][$groupName]->getClass() ?>" style="<?= sprintf( + 'width: %2$s; height: %2$s; margin-top: -%1$Fem;', + (float) substr($extrapolatedCircleWidth, 0, -2) / 2, + $extrapolatedCircleWidth + ); ?>"> +<?php else: ?> + <div class="circle-box" style="width: <?= $circleWidth; ?>;"> + <div class="outer-circle" style="<?= sprintf( + 'width: %2$s; height: %2$s; margin-top: -%1$Fem;', + (float) substr($circleWidth, 0, -2) / 2, + $circleWidth + ); ?>"> +<?php endif ?> + <?= $this->qlink( + '', + $timeInfo[1][$groupName]->getDetailUrl(), + array( + 'type' => $groupName, + 'timestamp<' => $timeInfo[0]->start->getTimestamp(), + 'timestamp>' => $timeInfo[0]->end->getTimestamp() + ), + array( + 'title' => sprintf( + $this->translate('List %u %s registered %s', 'timeline.link.title'), + $timeInfo[1][$groupName]->getValue(), + strtolower($labelAndColor['label']), + $titleTime + ), + 'class' => 'inner-circle ' . $timeInfo[1][$groupName]->getClass(), + 'style' => sprintf( + 'width: %2$s; height: %2$s; margin-top: -%1$Fem; margin-left: -%1$Fem;', + (float) substr($circleWidth, 0, -2) / 2, + (string) $circleWidth + ) + ) + ); ?> + </div> + </div> +<?php endif ?> +<?php endforeach ?> + </div> + <?php $firstRow = false; ?> +<?php endforeach ?> + <a aria-hidden="true" id="end" href="<?= Url::fromRequest()->remove( + array( + 'timestamp<', + 'timestamp>' + ) + )->overwriteParams( + array( + 'start' => $nextRange->getStart()->getTimestamp(), + 'end' => $nextRange->getEnd()->getTimestamp(), + 'extend' => 1 + ) + ); ?>"></a> +<?php if (!$beingExtended): ?> + </div> +</div> +<?php endif ?> diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php new file mode 100644 index 0000000..663db93 --- /dev/null +++ b/modules/monitoring/configuration.php @@ -0,0 +1,431 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +use Icinga\Authentication\Auth; + +/** @var $this \Icinga\Application\Modules\Module */ + +$this->providePermission( + 'monitoring/command/*', + $this->translate('Allow all commands') +); +$this->providePermission( + 'monitoring/command/schedule-check', + $this->translate('Allow scheduling host and service checks') +); +$this->providePermission( + 'monitoring/command/schedule-check/active-only', + $this->translate('Allow scheduling host and service checks (Only on objects with active checks enabled)') +); +$this->providePermission( + 'monitoring/command/acknowledge-problem', + $this->translate('Allow acknowledging host and service problems') +); +$this->providePermission( + 'monitoring/command/remove-acknowledgement', + $this->translate('Allow removing problem acknowledgements') +); +$this->providePermission( + 'monitoring/command/comment/*', + $this->translate('Allow adding and deleting host and service comments') +); +$this->providePermission( + 'monitoring/command/comment/add', + $this->translate('Allow commenting on hosts and services') +); +$this->providePermission( + 'monitoring/command/comment/delete', + $this->translate('Allow deleting host and service comments') +); +$this->providePermission( + 'monitoring/command/downtime/*', + $this->translate('Allow scheduling and deleting host and service downtimes') +); +$this->providePermission( + 'monitoring/command/downtime/schedule', + $this->translate('Allow scheduling host and service downtimes') +); +$this->providePermission( + 'monitoring/command/downtime/delete', + $this->translate('Allow deleting host and service downtimes') +); +$this->providePermission( + 'monitoring/command/process-check-result', + $this->translate('Allow processing host and service check results') +); +$this->providePermission( + 'monitoring/command/feature/instance', + $this->translate('Allow processing commands for toggling features on an instance-wide basis') +); +$this->providePermission( + 'monitoring/command/feature/object/*', + $this->translate('Allow processing commands for toggling features on host and service objects') +); +$this->providePermission( + 'monitoring/command/feature/object/active-checks', + $this->translate('Allow processing commands for toggling active checks on host and service objects') +); +$this->providePermission( + 'monitoring/command/feature/object/passive-checks', + $this->translate('Allow processing commands for toggling passive checks on host and service objects') +); +$this->providePermission( + 'monitoring/command/feature/object/notifications', + $this->translate('Allow processing commands for toggling notifications on host and service objects') +); +$this->providePermission( + 'monitoring/command/feature/object/event-handler', + $this->translate('Allow processing commands for toggling event handlers on host and service objects') +); +$this->providePermission( + 'monitoring/command/feature/object/flap-detection', + $this->translate('Allow processing commands for toggling flap detection on host and service objects') +); +$this->providePermission( + 'monitoring/command/send-custom-notification', + $this->translate('Allow sending custom notifications for hosts and services') +); +$this->providePermission( + 'no-monitoring/contacts', + $this->translate('Prohibit access to contacts and contactgroups') +); + +$this->provideRestriction( + 'monitoring/filter/objects', + $this->translate('Restrict views to the Icinga objects that match the filter') +); +$this->provideRestriction( + 'monitoring/blacklist/properties', + $this->translate('Hide the properties of monitored objects that match the filter') +); + +$this->provideConfigTab('backends', array( + 'title' => $this->translate('Configure how to retrieve monitoring information'), + 'label' => $this->translate('Backends'), + 'url' => 'config' +)); +$this->provideConfigTab('security', array( + 'title' => $this->translate('Configure how to protect your monitoring environment against prying eyes'), + 'label' => $this->translate('Security'), + 'url' => 'config/security' +)); +$this->provideSetupWizard('Icinga\Module\Monitoring\MonitoringWizard'); + +/* + * Available Search Urls + */ +$this->provideSearchUrl($this->translate('Tactical Overview'), 'monitoring/tactical', 100); +$this->provideSearchUrl($this->translate('Hosts'), 'monitoring/list/hosts?sort=host_severity&limit=10', 99); +$this->provideSearchUrl($this->translate('Services'), 'monitoring/list/services?sort=service_severity&limit=10', 98); +$this->provideSearchUrl($this->translate('Hostgroups'), 'monitoring/list/hostgroups?limit=10', 97); +$this->provideSearchUrl($this->translate('Servicegroups'), 'monitoring/list/servicegroups?limit=10', 96); + +/* + * Available navigation items + */ +$this->provideNavigationItem('host-action', $this->translate('Host Action')); +$this->provideNavigationItem('service-action', $this->translate('Service Action')); +// Notes are disabled as we're not sure whether to really make a difference between actions and notes +//$this->provideNavigationItem('host-note', $this->translate('Host Note')); +//$this->provideNavigationItem('service-note', $this->translate('Service Note')); + +/* + * Problems Section + */ +$section = $this->menuSection(N_('Problems'), array( + 'renderer' => array( + 'SummaryNavigationItemRenderer', + 'state' => 'critical' + ), + 'icon' => 'attention-circled', + 'priority' => 20 +)); +$section->add(N_('Host Problems'), array( + 'icon' => 'host', + 'description' => $this->translate('List current host problems'), + 'renderer' => array( + 'MonitoringBadgeNavigationItemRenderer', + 'columns' => array( + 'hosts_down_unhandled' => $this->translate('%d unhandled hosts down') + ), + 'state' => 'critical', + 'dataView' => 'unhandledhostproblems' + ), + 'url' => 'monitoring/list/hosts?host_problem=1&sort=host_severity', + 'priority' => 50 +)); +$section->add(N_('Service Problems'), array( + 'icon' => 'service', + 'description' => $this->translate('List current service problems'), + 'renderer' => array( + 'MonitoringBadgeNavigationItemRenderer', + 'columns' => array( + 'services_critical_unhandled' => $this->translate('%d unhandled services critical') + ), + 'state' => 'critical', + 'dataView' => 'unhandledserviceproblems' + ), + 'url' => 'monitoring/list/services?service_problem=1&sort=service_severity&dir=desc', + 'priority' => 60 +)); +$section->add(N_('Service Grid'), array( + 'icon' => 'services', + 'description' => $this->translate('Display service problems as grid'), + 'url' => 'monitoring/list/servicegrid?problems', + 'priority' => 70 +)); +$section->add(N_('Current Downtimes'), array( + 'icon' => 'plug', + 'description' => $this->translate('List current downtimes'), + 'url' => 'monitoring/list/downtimes?downtime_is_in_effect=1', + 'priority' => 80 +)); + +/* + * Overview Section + */ +$section = $this->menuSection(N_('Overview'), array( + 'icon' => 'binoculars', + 'priority' => 30 +)); +$section->add(N_('Tactical Overview'), array( + 'icon' => 'chart-pie', + 'description' => $this->translate('Open tactical overview'), + 'url' => 'monitoring/tactical', + 'priority' => 40 +)); +$section->add(N_('Hosts'), array( + 'icon' => 'host', + 'description' => $this->translate('List hosts'), + 'url' => 'monitoring/list/hosts', + 'priority' => 50 +)); +$section->add(N_('Services'), array( + 'icon' => 'service', + 'description' => $this->translate('List services'), + 'url' => 'monitoring/list/services', + 'priority' => 50 +)); +$section->add(N_('Servicegroups'), array( + 'icon' => 'services', + 'description' => $this->translate('List service groups'), + 'url' => 'monitoring/list/servicegroups', + 'priority' => 60 +)); +$section->add(N_('Hostgroups'), array( + 'icon' => 'host', + 'description' => $this->translate('List host groups'), + 'url' => 'monitoring/list/hostgroups', + 'priority' => 60 +)); + +// Checking the permission here since navigation items don't support negating permissions +$auth = Auth::getInstance(); +if ($auth->hasPermission('*') || ! $auth->hasPermission('no-monitoring/contacts')) { + $section->add(N_('Contacts'), array( + 'icon' => 'user', + 'description' => $this->translate('List contacts'), + 'url' => 'monitoring/list/contacts', + 'priority' => 70 + )); + $section->add(N_('Contactgroups'), array( + 'icon' => 'users', + 'description' => $this->translate('List users'), + 'url' => 'monitoring/list/contactgroups', + 'priority' => 70 + )); +} + +$section->add(N_('Comments'), array( + 'icon' => 'chat-empty', + 'description' => $this->translate('List comments'), + 'url' => 'monitoring/list/comments?comment_type=comment|comment_type=ack', + 'priority' => 80 +)); +$section->add(N_('Downtimes'), array( + 'icon' => 'plug', + 'description' => $this->translate('List downtimes'), + 'url' => 'monitoring/list/downtimes', + 'priority' => 80 +)); + +/* + * History Section + */ +$section = $this->menuSection(N_('History'), array( + 'icon' => 'history', + 'priority' => 90 +)); +$section->add(N_('Event Grid'), array( + 'icon' => 'history', + 'description' => $this->translate('Open event grid'), + 'priority' => 10, + 'url' => 'monitoring/list/eventgrid' +)); +$section->add(N_('Event Overview'), array( + 'icon' => 'history', + 'description' => $this->translate('Open event overview'), + 'priority' => 20, + 'url' => 'monitoring/list/eventhistory?timestamp>=-7%20days' +)); +$section->add(N_('Notifications'), array( + 'icon' => 'bell', + 'description' => $this->translate('List notifications'), + 'priority' => 30, + 'url' => 'monitoring/list/notifications?notification_timestamp>=-7%20days', +)); +$section->add(N_('Timeline'), array( + 'icon' => 'clock', + 'description' => $this->translate('Open timeline'), + 'priority' => 40, + 'url' => 'monitoring/timeline' +)); + +/* + * Reporting Section + */ +$section = $this->menuSection(N_('Reporting'), array( + 'icon' => 'barchart', + 'priority' => 100 +)); + +/* + * Current Incidents + */ +$dashboard = $this->dashboard(N_('Current Incidents'), array('priority' => 50)); +$dashboard->add( + N_('Service Problems'), + 'monitoring/list/services?service_problem=1&limit=10&sort=service_severity', + 100 +); +$dashboard->add( + N_('Recently Recovered Services'), + 'monitoring/list/services?service_state=0&limit=10&sort=service_last_state_change&dir=desc', + 110 +); +$dashboard->add( + N_('Host Problems'), + 'monitoring/list/hosts?host_problem=1&sort=host_severity', + 120 +); + +/* + * Overview + */ +//$dashboard = $this->dashboard(N_('Overview'), array('priority' => 60)); +//$dashboard->add( +// N_('Service Grid'), +// 'monitoring/list/servicegrid?limit=15,18' +//); +//$dashboard->add( +// N_('Service Groups'), +// 'monitoring/list/servicegroups' +//); +//$dashboard->add( +// N_('Host Groups'), +// 'monitoring/list/hostgroups' +//); + +/* + * Most Overdue + */ +$dashboard = $this->dashboard(N_('Overdue'), array('priority' => 70)); +$dashboard->add( + N_('Late Host Check Results'), + 'monitoring/list/hosts?host_next_update<now', + 100 +); +$dashboard->add( + N_('Late Service Check Results'), + 'monitoring/list/services?service_next_update<now', + 110 +); +$dashboard->add( + N_('Acknowledgements Active For At Least Three Days'), + 'monitoring/list/comments?comment_type=Ack&comment_timestamp<-3 days&sort=comment_timestamp&dir=asc', + 120 +); +$dashboard->add( + N_('Downtimes Active For More Than Three Days'), + 'monitoring/list/downtimes?downtime_is_in_effect=1&downtime_scheduled_start<-3%20days&sort=downtime_start&dir=asc', + 130 +); + +/* + * Muted Objects + */ +$dashboard = $this->dashboard(N_('Muted'), array('priority' => 80)); +$dashboard->add( + N_('Disabled Service Notifications'), + 'monitoring/list/services?service_notifications_enabled=0&limit=10', + 100 +); +$dashboard->add( + N_('Disabled Host Notifications'), + 'monitoring/list/hosts?host_notifications_enabled=0&limit=10', + 110 +); +$dashboard->add( + N_('Disabled Service Checks'), + 'monitoring/list/services?service_active_checks_enabled=0&limit=10', + 120 +); +$dashboard->add( + N_('Disabled Host Checks'), + 'monitoring/list/hosts?host_active_checks_enabled=0&limit=10', + 130 +); +$dashboard->add( + N_('Acknowledged Problem Services'), + 'monitoring/list/services?service_acknowledgement_type!=0&service_problem=1&sort=service_state&limit=10', + 140 +); +$dashboard->add( + N_('Acknowledged Problem Hosts'), + 'monitoring/list/hosts?host_acknowledgement_type!=0&host_problem=1&sort=host_severity&limit=10', + 150 +); + +/* + * Activity Stream + */ +//$dashboard = $this->dashboard(N_('Activity Stream'), array('priority' => 90)); +//$dashboard->add( +// N_('Recent Events'), +// 'monitoring/list/eventhistory?timestamp>=-3%20days&sort=timestamp&dir=desc&limit=8' +//); +//$dashboard->add( +// N_('Recent Hard State Changes'), +// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=hard_state&sort=timestamp&dir=desc&limit=8' +//); +//$dashboard->add( +// N_('Recent Notifications'), +// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=notify&sort=timestamp&dir=desc&limit=8' +//); +//$dashboard->add( +// N_('Downtimes Recently Started'), +// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_start&sort=timestamp&dir=desc&limit=8' +//); +//$dashboard->add( +// N_('Downtimes Recently Ended'), +// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_end&sort=timestamp&dir=desc&limit=8' +//); + +/* + * Stats + */ +//$dashboard = $this->dashboard(N_('Stats'), array('priority' => 99)); +//$dashboard->add( +// N_('Check Stats'), +// 'monitoring/health/stats' +//); +//$dashboard->add( +// N_('Process Information'), +// 'monitoring/health/info' +//); + +/* + * CSS + */ +$this->provideCssFile('service-grid.less'); +$this->provideCssFile('tables.less'); diff --git a/modules/monitoring/doc/01-About.md b/modules/monitoring/doc/01-About.md new file mode 100644 index 0000000..deb47bf --- /dev/null +++ b/modules/monitoring/doc/01-About.md @@ -0,0 +1,10 @@ +# About the Monitoring Module <a id="monitoring-module-about"></a> + +Please read the following chapters for more insights on this module: + +* [Installation](02-Installation.md#monitoring-module-installation) +* [Configuration](03-Configuration.md#monitoring-module-configuration) +* [Security](06-Security.md#monitoring-module-security) +* [Restrict Custom Variables](10-Restrict-Custom-Variables.md#monitoring-module-restrict-access-custom-variables) +* [Hooks](20-Hooks.md#monitoring-module-hooks) +* [Add Columns to List Views](11-Add-Columns-List-Views.md#monitoring-module-add-columns-list-views) diff --git a/modules/monitoring/doc/02-Installation.md b/modules/monitoring/doc/02-Installation.md new file mode 100644 index 0000000..43a7cd0 --- /dev/null +++ b/modules/monitoring/doc/02-Installation.md @@ -0,0 +1,15 @@ +# Monitoring Module Installation <a id="monitoring-module-installation"></a> + +This module is provided with the Icinga Web 2 package and does +not need any extra installation step. + +## Enable the Module <a id="monitoring-module-enable"></a> + +Navigate to `Configuration` -> `Modules` -> `monitoring` and enable +the module. + +You can also enable the module during the setup wizard, or on the CLI: + +``` +icingacli module enable monitoring +``` diff --git a/modules/monitoring/doc/03-Configuration.md b/modules/monitoring/doc/03-Configuration.md new file mode 100644 index 0000000..9adacc1 --- /dev/null +++ b/modules/monitoring/doc/03-Configuration.md @@ -0,0 +1,69 @@ +# Monitoring Module Configuration <a id="monitoring-module-configuration"></a> + +## Overview <a id="monitoring-module-configuration-overview"></a> + +The module specific configuration is stored in `/etc/icingaweb2/modules/monitoring`. + +File/Directory | Description +----------------------------------------------------------------------|--------------------------------- +config.ini | Security settings (e.g. protected custom vars) for the `monitoring` module | +[backends.ini](04-Backends.md#monitoring-module-backends) | Data backend (e.g. the IDO database [resource](../../../doc/04-Resources.md#resources-configuration-database) name). +[commandtransports.ini](05-Command-Transports.md) | Command transports for specific Icinga instances + + +## General Configuration <a id="monitoring-module-configuration-general"></a> + +Navigate into `Configuration` -> `Modules` -> `Monitoring`. This allows +you to see the provided [permissions and restrictions](06-Security.md#monitoring-security) +by this module. + +### Default Settings <a id="monitoring-module-configuration-settings"></a> + +Option | Description +----------------------------------|----------------------------------------------- +acknowledge_expire | **Optional.** Check "Use Expire Time" in Acknowledgement dialog by default. Defaults to **0 (false)**. +acknowledge_expire_time | **Optional.** Set default value for "Expire Time" in Acknowledgement dialog, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**. +acknowledge_notify | **Optional.** Check "Send Notification" in Acknowledgement dialog by default. Defaults to **1 (true)**. +acknowledge_persistent | **Optional.** Check "Persistent Comment" in Acknowledgement dialog by default. Defaults to **0 (false)**. +acknowledge_sticky | **Optional.** Check "Sticky Acknowledgement" in Acknowledgement dialog by default. Defaults to **0 (false)**. +comment_expire | **Optional.** Check "Use Expire Time" in Comment dialog by default. Defaults to **0 (false)**. +hostdowntime_comment_text | **Optional.** Set default text for "Comment" in Host Downtime dialog by default. +servicedowntime_comment_text | **Optional.** Set default text for "Comment" in Service Downtime dialog by default. +comment_expire_time | **Optional.** Set default value for "Expire Time" in Comment dialog, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**. +custom_notification_forced | **Optional.** Check "Forced" in Custom Notification dialog by default. Defaults to **0 (false)**. +hostcheck_all_services | **Optional.** Check "All Services" in Schedule Host Check dialog by default. Defaults to **0 (false)**. +hostdowntime_all_services | **Optional.** Check "All Services" in Schedule Host Downtime dialog by default. Defaults to **0 (false)**. +hostdowntime_end_fixed | **Optional.** Set default value for "End Time" in Schedule Host Downtime dialog for **Fixed** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**. +hostdowntime_end_flexible | **Optional.** Set default value for "End Time" in Schedule Host Downtime dialog for **Flexible** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**. +hostdowntime_flexible_duration | **Optional.** Set default value for "Flexible Duration" in Schedule Host Downtime dialog for **Flexible** downtime. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **2 hour (PT2H)**. +servicedowntime_end_fixed | **Optional.** Set default value for "End Time" in Schedule Service Downtime dialog for **Fixed** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**. +servicedowntime_end_flexible | **Optional.** Set default value for "End Time" in Schedule Service Downtime dialog for **Flexible** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**. +servicedowntime_flexible_duration | **Optional.** Set default value for "Flexible Duration" in Schedule Service Downtime dialog for **Flexible** downtime. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **2 hour (PT2H)**. + +Example for having acknowledgements with 2 hours expire time by default. + +``` +# vim /etc/icingaweb2/modules/monitoring/config.ini + +[settings] +acknowledge_expire = 1 +acknowledge_expire_time = PT2H + +``` + +### Security Configuration <a id="monitoring-module-configuration-security"></a> + +Option | Description +-------------------------|----------------------------------------------- +protected\_customvars | **Optional.** Comma separated list of string patterns for custom variables which should be excluded from user's view. + + +Example for custom variable names which match `*pw*` or `*pass*` or `community`. + +``` +# vim /etc/icingaweb2/modules/monitoring/config.ini + +[security] +protected_customvars = "*pw*,*pass*,community" +``` + diff --git a/modules/monitoring/doc/04-Backends.md b/modules/monitoring/doc/04-Backends.md new file mode 100644 index 0000000..2681109 --- /dev/null +++ b/modules/monitoring/doc/04-Backends.md @@ -0,0 +1,30 @@ +# Backends <a id="monitoring-module-backends"></a> + +The configuration file `backends.ini` contains information about data sources which are +used to fetch monitoring objects presented to the user. + +The required [resources](../../../doc/04-Resources.md#resources-configuration-database) must be globally defined beforehand. + +## Configuration <a id="monitoring-module-backends-configuration"></a> + +Navigate into `Configuration` -> `Modules` -> `Monitoring` -> `Backends`. +You can select a specified global resource here, and also update its details. + +Each section in `backends.ini` references a resource. By default you should only have one backend enabled. + +### IDO Backend <a id="monitoring-module-backends-ido"></a> + +Option | Description +-------------------------|----------------------------------------------- +type | **Required.** Specify the backend type. Must be set to `ido`. +resource | **Required.** Specify a defined [resource](../../../doc/04-Resources.md#resources-configuration-database) name which provides details about the IDO database resource. + + +Example for using the database resource `icinga2_ido_mysql`: + +``` +[icinga2_ido_mysql] +type = "ido" +resource = "icinga2_ido_mysql" +``` + diff --git a/modules/monitoring/doc/05-Command-Transports.md b/modules/monitoring/doc/05-Command-Transports.md new file mode 100644 index 0000000..bdf9f56 --- /dev/null +++ b/modules/monitoring/doc/05-Command-Transports.md @@ -0,0 +1,185 @@ +# External Command Transport Configuration <a id="monitoring-module-commandtransports"></a> + +## Configuration <a id="monitoring-module-commandtransports-configuration"></a> + +Navigate into `Configuration` -> `Modules` -> `Monitoring` -> `Backends`. +You can create/edit command transports here. + +The `commandtransports.ini` configuration file defines how Icinga Web 2 +transports commands to your Icinga instance in order to submit +external commands. By default, this file is located at `/etc/icingaweb2/modules/monitoring/commandtransports.ini`. + +You can define multiple command transports in the `commandtransports.ini` file. Every transport starts with a section header +containing its name, followed by the config directives for this transport in the standard INI-format. + +Icinga Web 2 will try one transport after another to send a command until the command is successfully sent. +If [configured](05-Command-Transports.md#commandtransports-multiple-instances), Icinga Web 2 will take different instances into account. +The order in which Icinga Web 2 processes the configured transports is defined by the order of sections in +`commandtransports.ini`. + +## Use the Icinga 2 API <a id="commandtransports-icinga2-api"></a> + +If you're running Icinga 2 it's best to use the [Icinga 2 API](https://icinga.com/docs/icinga2/latest/doc/12-icinga2-api/) +for transmitting external commands. + +### Icinga 2 Preparations <a id="commandtransports-icinga2-api-preparations"></a> + +You have to run the `api` setup on the Icinga 2 host where you want to send the commands to: + +``` +icinga2 api setup +``` + +Next, you have to create an ApiUser object for authenticating against the Icinga 2 API. This configuration also applies +to the host where you want to send the commands to. We recommend to create/edit the file +`/etc/icinga2/conf.d/api-users.conf`: + +``` +object ApiUser "icingaweb2" { + password = "bea11beb7b810ea9ce6ea" // Change this! + permissions = [ "status/query", "actions/*", "objects/modify/*", "objects/query/*" ] +} +``` + +The permissions are mandatory in order to submit all external commands from within Icinga Web 2. + +**Restart Icinga 2** for the changes to take effect. + +``` +systemctl restart icinga2 +``` + +### Configuration in Icinga Web 2 <a id="commandtransports-icinga2-api-configuration"></a> + +> **Note** +> +> Please make sure that your server running Icinga Web 2 has the `PHP cURL` extension installed and enabled. + +The Icinga 2 API requires the following settings: + +Option | Description +-------------------------|----------------------------------------------- +transport | **Required.** The transport type. Must be set to `api`. +host | **Required.** The host address where the Icinga 2 API is listening on. +port | **Required.** The port where the Icinga 2 API is listening on. Defaults to `5665`. +username | **Required.** Basic auth username. +password | **Required.** Basic auth password. + +Example: + +``` +# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini + +[icinga2] +transport = "api" +host = "127.0.0.1" ; Icinga 2 host +port = "5665" +username = "icingaweb2" +password = "bea11beb7b810ea9ce6ea" ; Change this! +``` + +## Use a Local Command Pipe <a id="commandtransports-local-command-pipe"></a> + +A local Icinga instance requires the following settings: + +Option | Description +-------------------------|----------------------------------------------- +transport | **Required.** The transport type. Must be set to `local`. +path | **Required.** The absolute path to the local command pipe. + +Example: + +``` +# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini + +[icinga2] +transport = local +path = /var/run/icinga2/cmd/icinga2.cmd +``` + +When commands are being sent to the Icinga instance, Icinga Web 2 opens the file found +on the local filesystem underneath `path` and writes the external command to it. + +Please note that errors are not returned using this method. The Icinga 2 API sends +error feedback. + +## Use SSH For a Remote Command Pipe <a id="commandtransports-ssh-remote-command-pipe"></a> + +A command pipe on a remote host's filesystem can be accessed by configuring a +SSH based command transport and requires the following settings: + +Option | Description +-------------------------|----------------------------------------------- +transport | **Required.** The transport type. Must be set to `remote`. +path | **Required.** The path on the remote server to its local command pipe. +host | **Required.** The SSH host. +port | **Optional.** The SSH port. Defaults to `22`. +user | **Required.** The SSH auth user. +resource | **Optional.** The SSH [resource](../../../doc/04-Resources.md#resources-configuration-ssh) +instance | **Optional.** The Icinga instance name. Only required for multiple instances. + +Example: + +``` +# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini + +[icinga2] +transport = remote +path = /var/run/icinga2/cmd/icinga2.cmd +host = example.tld +user = icinga +;port = 22 ; Optional. The default is 22 +``` + +To make this example work, you'll need to permit your web-server's user +public-key based access to the defined remote host so that Icinga Web 2 can +connect to it and login as the defined user. + +You can also make use of a dedicated SSH resource to permit access for a +different user than the web-server's one. This way, you can provide a private +key file on the local filesystem that is used to access the remote host. + +To accomplish this, a new resource is required that is defined in your +transport's configuration instead of a user: + +``` +# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini + +[icinga2] +transport = remote +path = /var/run/icinga2/cmd/icinga2.cmd +host = example.tld +resource = example.tld-icinga2 +;port = 22 ; Optional. The default is 22 +``` + +The resource's configuration needs to be put into the resources.ini file: + +``` +# vim /etc/icingaweb2/resources.ini + +[example.tld-icinga2] +type = ssh +user = icinga +private_key = /etc/icingaweb2/ssh/icinga +``` + +## Configure Transports for Different Icinga Instances <a id="commandtransports-multiple-instances"></a> + +If there are multiple but different Icinga instances writing to your IDO database, +you can define which transport belongs to which Icinga instance by providing the +`instance` setting. This setting must specify the name of the Icinga +instance you want to assign to the transport: + +``` +[icinga1] +... +instance = icinga1 + +[icinga2] +... +instance = icinga2 +``` + +Associating a transport to a specific Icinga instance causes this transport to be used to send commands to the linked +instance only. Transports without a linked Icinga instance are used to send commands to all instances. diff --git a/modules/monitoring/doc/06-Security.md b/modules/monitoring/doc/06-Security.md new file mode 100644 index 0000000..750eaef --- /dev/null +++ b/modules/monitoring/doc/06-Security.md @@ -0,0 +1,66 @@ +# Security <a id="monitoring-module-security"></a> + +The monitoring module provides an additional set of restrictions and permissions +that can be used for access control. The following sections will list those +restrictions and permissions in detail: + + +## Permissions <a id="monitoring-module-security-permissions"></a> + +The monitoring module allows to send commands to an Icinga 2 instance. +A user needs specific permissions to be able to send those commands +when using the monitoring module. + + +Name | Permits +-------------------------------------------------|----------------------------------------------- +monitoring/command/* | Allow all commands. +monitoring/command/schedule-check | Allow scheduling host and service checks. +monitoring/command/schedule-check/active-only | Allow scheduling host and service checks. (Only on objects with active checks enabled) +monitoring/command/acknowledge-problem | Allow acknowledging host and service problems. +monitoring/command/remove-acknowledgement | Allow removing problem acknowledgements. +monitoring/command/comment/* | Allow adding and deleting host and service comments. +monitoring/command/comment/add | Allow commenting on hosts and services. +monitoring/command/comment/delete | Allow deleting host and service comments. +monitoring/command/downtime/* | Allow scheduling and deleting host and service downtimes. +monitoring/command/downtime/schedule | Allow scheduling host and service downtimes. +monitoring/command/downtime/delete | Allow deleting host and service downtimes. +monitoring/command/process-check-result | Allow processing host and service check results. +monitoring/command/feature/instance | Allow processing commands for toggling features on an instance-wide basis. +monitoring/command/feature/object/* | Allow processing commands for toggling features on host and service objects. +monitoring/command/feature/object/active-checks | Allow processing commands for toggling active checks on host and service objects. +monitoring/command/feature/object/passive-checks | Allow processing commands for toggling passive checks on host and service objects. +monitoring/command/feature/object/notifications | Allow processing commands for toggling notifications on host and service objects. +monitoring/command/feature/object/event-handler | Allow processing commands for toggling event handlers on host and service objects. +monitoring/command/feature/object/flap-detection | Allow processing commands for toggling flap detection on host and service objects. +monitoring/command/send-custom-notification | Allow sending custom notifications for hosts and services. + + +## Restrictions <a id="monitoring-module-security-restrictions"></a> + +The monitoring module allows filtering objects: + + +Keys | Restricts +--------------------------------------------|----------------------------------------------- +monitoring/filter/objects | Applies a filter to all hosts and services. + + +This filter will affect all hosts and services. Furthermore, it will also +affect all related objects, like notifications, downtimes and events. If a +service is hidden, all notifications, downtimes on that service will be hidden too. + + +### Filter Column Names <a id="monitoring-module-security-restrictions-filter-column-names"></a> + +The following filter column names are available in filter expressions: + + +Column | Description +-----------------------------------------------------------|----------------------------------------------- +instance\_name | Filter on an Icinga 2 instance. +host\_name | Filter on host object names. +hostgroup\_name | Filter on hostgroup object names. +service\_description | Filter on service object names. +servicegroup\_name | Filter on servicegroup object names. +all custom variables prefixed with `_host_` or `_service_` | Filter on specified custom variables. diff --git a/modules/monitoring/doc/10-Restrict-Custom-Variables.md b/modules/monitoring/doc/10-Restrict-Custom-Variables.md new file mode 100644 index 0000000..8d3a3b1 --- /dev/null +++ b/modules/monitoring/doc/10-Restrict-Custom-Variables.md @@ -0,0 +1,77 @@ +# Restrict Access to Custom Variables <a id="monitoring-module-restrict-access-custom-variables"></a> + +* Restriction name: monitoring/blacklist/properties +* Restriction value: Comma separated list of GLOB like filters + +Imagine the following host custom variable structure. + +``` +host.vars. +|-- cmdb_name +|-- cmdb_id +|-- cmdb_location +|-- wiki_id +|-- passwords. +| |-- mysql_password +| |-- ldap_password +| `-- mongodb_password +|-- legacy. +| |-- cmdb_name +| |-- mysql_password +| `-- wiki_id +`-- backup. + `-- passwords. + |-- mysql_password + `-- ldap_password +``` + +`host.vars.cmdb_name` + +Blacklists `cmdb_name` in the first level of the custom variable structure only. +`host.vars.legacy.cmdb_name` is not blacklisted. + + +`host.vars.cmdb_*` + +All custom variables in the first level of the structure which begin with `cmdb_` become blacklisted. +Deeper custom variables are ignored. `host.vars.legacy.cmdb_name` is not blacklisted. + +`host.vars.*id` + +All custom variables in the first level of the structure which end with `id` become blacklisted. +Deeper custom variables are ignored. `host.vars.legacy.wiki_id` is not blacklisted. + +`host.vars.*.mysql_password` + +Matches all custom variables on the second level which are equal to `mysql_password`. + +`host.vars.*.*password` + +Matches all custom variables on the second level which end with `password`. + +`host.vars.*.mysql_password,host.vars.*.ldap_password` + +Matches all custorm variables on the second level which equal `mysql_password` or `ldap_password`. + +`host.vars.**.*password` + +Matches all custom variables on all levels which end with `password`. + +Please note the two asterisks, `**`, here for crossing level boundaries. This syntax is used for matching the complete +custom variable structure. + +If you want to restrict all custom variables that end with password for both hosts and services, you have to define +the following restriction. + +`host.vars.**.*password,service.vars.**.*password` + +## Escape Meta Characters <a id="restrict-access-custom-variables-escape-meta-chars"></a> + +Use backslash to escape the meta characters + +* * +* , + +`host.vars.\*fall` + +Matches all custom variables in the first level which equal `*fall`. diff --git a/modules/monitoring/doc/11-Add-Columns-List-Views.md b/modules/monitoring/doc/11-Add-Columns-List-Views.md new file mode 100644 index 0000000..2567ead --- /dev/null +++ b/modules/monitoring/doc/11-Add-Columns-List-Views.md @@ -0,0 +1,32 @@ +# Add Columns to List Views <a id="monitoring-module-add-columns-list-views"></a> + +The monitoring module provides list views for hosts and services. +These lists only provide the most common columns to reduce the backend +query load. + +If you want to add more columns to the list view e.g. in order to use the URL in +your dashboards or as external iframe integration, you need the `addColumns` URL +parameter. + + + +Example for adding the host `address` attribute in a host list: + +``` +http://localhost/icingaweb2/monitoring/list/hosts?addColumns=host_address +``` + +![Screenshot](img/list_hosts_add_columns.png) + + + + +Example for multiple columns as comma separated parameter string. This +includes a reference to the Icinga 2 host object custom attribute `os` using +`_host_` as custom variable identifier. + +``` +http://localhost/icingaweb2/monitoring/list/services?addColumns=host_address,_host_os +``` + +![Screenshot](img/list_services_add_columns.png) diff --git a/modules/monitoring/doc/20-Hooks.md b/modules/monitoring/doc/20-Hooks.md new file mode 100644 index 0000000..5d38843 --- /dev/null +++ b/modules/monitoring/doc/20-Hooks.md @@ -0,0 +1,161 @@ +# Monitoring Module Hooks <a id="monitoring-module-hooks"></a> + +## Detail View Extension Hook <a id="monitoring-module-hooks-detailviewextension"></a> + +This hook can be used to easily extend the detail view of monitored objects (hosts and services). + +### How it works <a id="monitoring-module-hooks-detailviewextension-how-it-works"></a> + +#### Directory structure <a id="monitoring-module-hooks-detailviewextension-directory-structure"></a> + +* `icingaweb2/modules/example` + * `library/Example/ProvidedHook/Monitoring/DetailviewExtension/Simple.php` + * `run.php` + +#### Files <a id="monitoring-module-hooks-detailviewextension-files"></a> + +##### run.php <a id="monitoring-module-hooks-detailviewextension-files-run-php"></a> + +```php +<?php +/** @var \Icinga\Application\Modules\Module $this */ + +$this->provideHook( + 'monitoring/DetailviewExtension', + 'Icinga\Module\Example\ProvidedHook\Monitoring\DetailviewExtension\Simple' +); +``` + +##### Simple.php <a id="monitoring-module-hooks-detailviewextension-files-simple-php"></a> + +```php +<?php +namespace Icinga\Module\Example\ProvidedHook\Monitoring\DetailviewExtension; + +use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +class Simple extends DetailviewExtensionHook +{ + public function getHtmlForObject(MonitoredObject $object) + { + $stats = array(); + foreach (str_split($object->name) as $c) { + if (isset($stats[$c])) { + ++$stats[$c]; + } else { + $stats[$c] = 1; + } + } + + ksort($stats); + + $view = $this->getView(); + + $thead = ''; + $tbody = ''; + foreach ($stats as $c => $amount) { + $thead .= '<th>' . $view->escape($c) . '</th>'; + $tbody .= '<td>' . $amount . '</td>'; + } + + return '<h2>' + . $view->escape(sprintf($view->translate('A %s named "%s"'), $object->getType(), $object->name)) + . '</h2>' + . '<h3>Character stats</h3>' + . '<table>' + . '<thead>' . $thead . '</thead>' + . '<tbody>' . $tbody . '</tbody>' + . '</table>'; + } +} +``` + +### How it looks <a id="monitoring-module-hooks-detailviewextension-how-it-looks"></a> + +![Screenshot](img/hooks-detailviewextension-01.png) + +## Plugin Output Hook <a id="monitoring-module-hooks-pluginoutput"></a> + +The Plugin Output Hook allows you to rewrite the plugin output based on check commands. You have to implement the +following methods: + +* `getCommands()` +* and `render()` + +With `getCommands()` you specify for which commands the provided hook is responsible for. You may return a single +command as string or a list of commands as array. If you want your hook to be responsible for every command, you have to +specify the `*`. + +In `render()` you rewrite the plugin output based on check commands. The parameter `$command` specifies the check +command of the host or service and `$output` specifies the plugin output. The parameter `$detail` tells you +whether the output is requested from the detail area of the host or service. + +Do not use complex logic for rewriting plugin output in list views because of the performance impact! + +You have to return the rewritten plugin output as string. It is also possible to return a HTML string here. +Please refer to `\Icinga\Module\Monitoring\Web\Helper\PluginOutputPurifier` for a list of allowed tags. + +Please also have a look at the following examples. + +**Example hook which is responsible for disk checks:** + +```php +<?php + +namespace Icinga\Module\Example\ProvidedHook\Monitoring; + +use Icinga\Module\Monitoring\Hook\PluginOutputHook; + +class PluginOutput extends PluginOutputHook +{ + public function getCommands() + { + return ['disk']; + } + + public function render($command, $output, $detail) + { + if (! $detail) { + // Don't rewrite plugin output in list views + return $output; + } + return implode('<br>', explode(';', $output)); + } +} +``` + +**Example hook which is responsible for disk and procs checks:** + +```php +<?php + +namespace Icinga\Module\Example\ProvidedHook\Monitoring; + +use Icinga\Module\Monitoring\Hook\PluginOutputHook; + +class PluginOutput extends PluginOutputHook +{ + public function getCommands() + { + return ['disk', 'procs']; + } + + public function render($command, $output, $detail) + { + switch ($command) { + case 'disk': + if ($detail) { + // Only rewrite plugin output in the detail area + $output = implode('<br>', explode(';', $output)); + } + break; + case 'procs': + $output = preg_replace('/(\d)+/', '<b>$1</b>', $output); + break; + } + + return $output; + } +} +``` diff --git a/modules/monitoring/doc/img/hooks-detailviewextension-01.png b/modules/monitoring/doc/img/hooks-detailviewextension-01.png Binary files differnew file mode 100644 index 0000000..a5ddaf1 --- /dev/null +++ b/modules/monitoring/doc/img/hooks-detailviewextension-01.png diff --git a/modules/monitoring/doc/img/list_hosts_add_columns.png b/modules/monitoring/doc/img/list_hosts_add_columns.png Binary files differnew file mode 100644 index 0000000..874a8f1 --- /dev/null +++ b/modules/monitoring/doc/img/list_hosts_add_columns.png diff --git a/modules/monitoring/doc/img/list_services_add_columns.png b/modules/monitoring/doc/img/list_services_add_columns.png Binary files differnew file mode 100644 index 0000000..dd0db82 --- /dev/null +++ b/modules/monitoring/doc/img/list_services_add_columns.png diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php b/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php new file mode 100644 index 0000000..71fc6a1 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php @@ -0,0 +1,10 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido; + +use Icinga\Module\Monitoring\Backend\MonitoringBackend; + +class IdoBackend extends MonitoringBackend +{ +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php new file mode 100644 index 0000000..09779b6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php @@ -0,0 +1,74 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Zend_Db_Select; + +class AllcontactsQuery extends IdoQuery +{ + protected $columnMap = array( + 'contacts' => array( + 'contact_name' => 'c.contact_name', + 'host_object_id' => 'c.host_object_id', + 'host_name' => 'c.host_name', + 'service_object_id' => 'c.service_object_id', + 'service_host_name' => 'c.service_host_name', + 'service_description' => 'c.service_description', + + 'contact_alias' => 'c.contact_alias', + 'contact_email' => 'c.contact_email', + 'contact_pager' => 'c.contact_pager', + 'contact_has_host_notfications' => 'c.contact_has_host_notfications', + 'contact_has_service_notfications' => 'c.contact_has_service_notfications', + 'contact_can_submit_commands' => 'c.contact_can_submit_commands', + 'contact_notify_service_recovery' => 'c.notify_service_recovery', + 'contact_notify_service_warning' => 'c.notify_service_warning', + 'contact_notify_service_critical' => 'c.notify_service_critical', + 'contact_notify_service_unknown' => 'c.notify_service_unknown', + 'contact_notify_service_flapping' => 'c.notify_service_flapping', + 'contact_notify_service_downtime' => 'c.notify_service_downtime', + 'contact_notify_host_recovery' => 'c.notify_host_recovery', + 'contact_notify_host_down' => 'c.notify_host_down', + 'contact_notify_host_unreachable' => 'c.notify_host_unreachable', + 'contact_notify_host_flapping' => 'c.notify_host_flapping', + 'contact_notify_host_downtime' => 'c.notify_host_downtime', + + + ) + ); + + protected $contacts; + protected $contactgroups; + protected $useSubqueryCount = true; + + public function requireColumn($alias) + { + $this->contacts->addColumn($alias); + $this->contactgroups->addColumn($alias); + return parent::requireColumn($alias); + } + + protected function joinBaseTables() + { + $this->contacts = $this->createSubQuery( + 'contact', + array('contact_name') + ); + $this->contactgroups = $this->createSubQuery( + 'contactgroup', + array('contact_name') + ); + $sub = $this->db->select()->union( + array($this->contacts, $this->contactgroups), + Zend_Db_Select::SQL_UNION_ALL + ); + + $this->baseQuery = $this->db->select()->distinct()->from( + array('c' => $sub), + array() + ); + + $this->joinedVirtualTables = array('contacts' => true); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php new file mode 100644 index 0000000..59a4ccb --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php @@ -0,0 +1,61 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for commands + */ +class CommandQuery extends IdoQuery +{ + /** + * @var array + */ + protected $columnMap = array( + 'commands' => array( + 'command_id' => 'c.command_id', + 'command_instance_id' => 'c.instance_id', + 'command_config_type' => 'c.config_type', + 'command_line' => 'c.command_line', + 'command_name' => 'co.name1' + ), + + 'contacts' => array( + 'contact_id' => 'con.contact_id', + 'contact_alias' => 'con.contact_alias' + ) + ); + + /** + * Fetch basic information about commands + */ + protected function joinBaseTables() + { + $this->select->from( + array('c' => $this->prefix . 'commands'), + array() + )->join( + array('co' => $this->prefix . 'objects'), + 'co.object_id = c.object_id', + array() + ); + + $this->joinedVirtualTables = array('commands' => true); + } + + /** + * Join contacts + */ + protected function joinContacts() + { + $this->select->join( + array('cnc' => $this->prefix . 'contact_notificationcommands'), + 'cnc.command_object_id = co.object_id', + array() + )->join( + array('con' => $this->prefix . 'contacts'), + 'con.contact_id = cnc.contact_id', + array() + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php new file mode 100644 index 0000000..6c01931 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php @@ -0,0 +1,158 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service comments + */ +class CommentQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'comments' => array( + 'comment_author' => 'c.comment_author', + 'comment_author_name' => 'c.comment_author_name', + 'comment_data' => 'c.comment_data', + 'comment_expiration' => 'c.comment_expiration', + 'comment_internal_id' => 'c.comment_internal_id', + 'comment_is_persistent' => 'c.comment_is_persistent', + 'comment_name' => 'c.comment_name', + 'comment_timestamp' => 'c.comment_timestamp', + 'comment_type' => 'c.comment_type', + 'instance_name' => 'c.instance_name', + 'object_type' => 'c.object_type' + ), + 'hosts' => array( + 'host_display_name' => 'c.host_display_name', + 'host_name' => 'c.host_name', + 'host_state' => 'c.host_state' + ), + 'services' => array( + 'service_description' => 'c.service_description', + 'service_display_name' => 'c.service_display_name', + 'service_host_name' => 'c.service_host_name', + 'service_state' => 'c.service_state' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $commentQuery; + + /** + * Subqueries used for the comment query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.14.0', '<')) { + $this->columnMap['comments']['comment_name'] = '(NULL)'; + } + $this->commentQuery = $this->db->select(); + $this->select->from( + array('c' => $this->commentQuery), + array() + ); + $this->joinedVirtualTables['comments'] = true; + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys($this->columnMap['comments'] + $this->columnMap['hosts']); + foreach (array_keys($this->columnMap['services']) as $column) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + $hosts = $this->createSubQuery('hostcomment', $columns); + $this->subQueries[] = $hosts; + $this->commentQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys($this->columnMap['comments'] + $this->columnMap['hosts'] + $this->columnMap['services']); + $services = $this->createSubQuery('servicecomment', $columns); + $this->subQueries[] = $services; + $this->commentQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php new file mode 100644 index 0000000..8cb4ddb --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service comment removal records + */ +class CommentdeletionhistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'commenthistory' => array( + 'id' => 'cdh.id', + 'object_type' => 'cdh.object_type' + ), + 'history' => array( + 'type' => 'cdh.type', + 'timestamp' => 'cdh.timestamp', + 'object_id' => 'cdh.object_id', + 'state' => 'cdh.state', + 'output' => 'cdh.output' + ), + 'hosts' => array( + 'host_display_name' => 'cdh.host_display_name', + 'host_name' => 'cdh.host_name' + ), + 'services' => array( + 'service_description' => 'cdh.service_description', + 'service_display_name' => 'cdh.service_display_name', + 'service_host_name' => 'cdh.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $commentDeletionHistoryQuery; + + /** + * Subqueries used for the comment history query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Whether to additionally select all history columns + * + * @var bool + */ + protected $fetchHistoryColumns = false; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->commentDeletionHistoryQuery = $this->db->select(); + $this->select->from( + array('cdh' => $this->commentDeletionHistoryQuery), + array() + ); + $this->joinedVirtualTables['commenthistory'] = true; + } + + /** + * Join history related columns and tables + */ + protected function joinHistory() + { + // TODO: Ensure that one is selecting the history columns first... + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + $this->requireVirtualTable('services'); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['commenthistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hostcommentdeletionhistory', $columns); + $this->subQueries[] = $hosts; + $this->commentDeletionHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['commenthistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Servicecommentdeletionhistory', $columns); + $this->subQueries[] = $services; + $this->commentDeletionHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php new file mode 100644 index 0000000..c85adff --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php @@ -0,0 +1,39 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host and service comment entry and deletion events + */ +class CommenteventQuery extends IdoQuery +{ + protected $columnMap = array( + 'commentevent' => array( + 'commentevent_id' => 'ch.commenthistory_id', + 'commentevent_entry_type' => "(CASE ch.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'downtime' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' ELSE NULL END)", + 'commentevent_comment_time' => 'UNIX_TIMESTAMP(ch.comment_time)', + 'commentevent_author_name' => 'ch.author_name', + 'commentevent_comment_data' => 'ch.comment_data', + 'commentevent_is_persistent' => 'ch.is_persistent', + 'commentevent_comment_source' => "(CASE ch.comment_source WHEN 0 THEN 'icinga' WHEN 1 THEN 'user' ELSE NULL END)", + 'commentevent_expires' => 'ch.expires', + 'commentevent_expiration_time' => 'UNIX_TIMESTAMP(ch.expiration_time)', + 'commentevent_deletion_time' => 'UNIX_TIMESTAMP(ch.deletion_time)' + ), + 'object' => array( + 'host_name' => 'o.name1', + 'service_description' => 'o.name2' + ) + ); + + protected function joinBaseTables() + { + $this->select() + ->from(array('ch' => $this->prefix . 'commenthistory'), array()) + ->join(array('o' => $this->prefix . 'objects'), 'ch.object_id = o.object_id', array()); + + $this->joinedVirtualTables['commentevent'] = true; + $this->joinedVirtualTables['object'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php new file mode 100644 index 0000000..47dd97c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service comment history records + */ +class CommenthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'commenthistory' => array( + 'id' => 'ch.id', + 'object_type' => 'ch.object_type' + ), + 'history' => array( + 'type' => 'ch.type', + 'timestamp' => 'ch.timestamp', + 'object_id' => 'ch.object_id', + 'state' => 'ch.state', + 'output' => 'ch.output' + ), + 'hosts' => array( + 'host_display_name' => 'ch.host_display_name', + 'host_name' => 'ch.host_name' + ), + 'services' => array( + 'service_description' => 'ch.service_description', + 'service_display_name' => 'ch.service_display_name', + 'service_host_name' => 'ch.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $commentHistoryQuery; + + /** + * Subqueries used for the comment history query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Whether to additionally select all history columns + * + * @var bool + */ + protected $fetchHistoryColumns = false; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->commentHistoryQuery = $this->db->select(); + $this->select->from( + array('ch' => $this->commentHistoryQuery), + array() + ); + $this->joinedVirtualTables['commenthistory'] = true; + } + + /** + * Join history related columns and tables + */ + protected function joinHistory() + { + // TODO: Ensure that one is selecting the history columns first... + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + $this->requireVirtualTable('services'); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['commenthistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hostcommenthistory', $columns); + $this->subQueries[] = $hosts; + $this->commentHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['commenthistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Servicecommenthistory', $columns); + $this->subQueries[] = $services; + $this->commentHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php new file mode 100644 index 0000000..ca10323 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php @@ -0,0 +1,139 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for contacts + */ +class ContactQuery extends IdoQuery +{ + protected $columnMap = [ + 'contacts' => [ + 'contact_id' => 'c.contact_id', + 'contact' => 'c.contact', + 'contact_name' => 'c.contact_name', + 'contact_alias' => 'c.contact_alias', + 'contact_email' => 'c.contact_email', + 'contact_pager' => 'c.contact_pager', + 'contact_object_id' => 'c.contact_object_id', + 'contact_has_host_notfications' => 'c.contact_has_host_notfications', + 'contact_has_service_notfications' => 'c.contact_has_service_notfications', + 'contact_can_submit_commands' => 'c.contact_can_submit_commands', + 'contact_notify_service_recovery' => 'c.contact_notify_service_recovery', + 'contact_notify_service_warning' => 'c.contact_notify_service_warning', + 'contact_notify_service_critical' => 'c.contact_notify_service_critical', + 'contact_notify_service_unknown' => 'c.contact_notify_service_unknown', + 'contact_notify_service_flapping' => 'c.contact_notify_service_flapping', + 'contact_notify_service_downtime' => 'c.contact_notify_service_downtime', + 'contact_notify_host_recovery' => 'c.contact_notify_host_recovery', + 'contact_notify_host_down' => 'c.contact_notify_host_down', + 'contact_notify_host_unreachable' => 'c.contact_notify_host_unreachable', + 'contact_notify_host_flapping' => 'c.contact_notify_host_flapping', + 'contact_notify_host_downtime' => 'c.contact_notify_host_downtime', + 'contact_notify_host_timeperiod' => 'c.contact_notify_host_timeperiod', + 'contact_notify_service_timeperiod' => 'c.contact_notify_service_timeperiod' + ] + ]; + + /** @var Zend_Db_Select The union */ + protected $contactQuery; + + /** @var IdoQuery[] Subqueries used for the contact query */ + protected $subQueries = []; + + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + public function addFilter(Filter $filter) + { + $strangers = array_diff( + $filter->listFilteredColumns(), + array_keys($this->columnMap['contacts']) + ); + if (! empty($strangers)) { + $this->transformToUnion(); + } + + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + + return $this; + } + + protected function joinBaseTables() + { + $this->contactQuery = $this->createSubQuery('Hostcontact', array_keys($this->columnMap['contacts'])); + $this->contactQuery->setIsSubQuery(); + $this->subQueries[] = $this->contactQuery; + + $this->select->from( + ['c' => $this->contactQuery], + [] + ); + + $this->joinedVirtualTables['contacts'] = true; + } + + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + + return parent::order($columnOrAlias, $dir); + } + + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } + + public function transformToUnion() + { + $this->contactQuery = $this->db->select(); + $this->select->reset(); + $this->subQueries = []; + + $this->select->distinct()->from( + ['c' => $this->contactQuery], + [] + ); + + $hosts = $this->createSubQuery('Hostcontact', array_keys($this->columnMap['contacts'])); + $this->subQueries[] = $hosts; + $this->contactQuery->union([$hosts], Zend_Db_Select::SQL_UNION_ALL); + + $services = $this->createSubQuery('Servicecontact', array_keys($this->columnMap['contacts'])); + $this->subQueries[] = $services; + $this->contactQuery->union([$services], Zend_Db_Select::SQL_UNION_ALL); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php new file mode 100644 index 0000000..7d4cbc1 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php @@ -0,0 +1,214 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for contact groups + */ +class ContactgroupQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('contactgroups' => array('cg.contactgroup_id', 'cgo.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hosts', 'members', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'contactgroups' => array( + 'contactgroup' => 'cgo.name1 COLLATE latin1_general_ci', + 'contactgroup_name' => 'cgo.name1', + 'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci' + ), + 'members' => array( + 'contact_count' => 'SUM(CASE WHEN cgmo.object_id IS NOT NULL THEN 1 ELSE 0 END)' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('cg' => $this->prefix . 'contactgroups'), + array() + )->join( + array('cgo' => $this->prefix . 'objects'), + 'cgo.object_id = cg.contactgroup_object_id AND cgo.is_active = 1 AND cgo.objecttype_id = 11', + array() + ); + $this->joinedVirtualTables['contactgroups'] = true; + } + + /** + * Join contact group members + */ + protected function joinMembers() + { + $this->select->joinLeft( + array('cgm' => $this->prefix . 'contactgroup_members'), + 'cgm.contactgroup_id = cg.contactgroup_id', + array() + )->joinLeft( + array('cgmo' => $this->prefix . 'objects'), + 'cgmo.object_id = cgm.contact_object_id AND cgmo.is_active = 1 AND cgmo.objecttype_id = 10', + array() + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('hosts'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->joinLeft( + array('hcg' => $this->prefix . 'host_contactgroups'), + 'hcg.contactgroup_object_id = cg.contactgroup_object_id', + array() + )->joinLeft( + array('h' => $this->prefix . 'hosts'), + 'h.host_id = hcg.host_id', + array() + )->joinLeft( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = h.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = cg.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.servicegroup_id = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('scg' => $this->prefix . 'service_contactgroups'), + 'scg.contactgroup_object_id = cg.contactgroup_object_id', + array() + )->joinLeft( + array('s' => $this->prefix . 'services'), + 's.service_id = scg.service_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $this->requireVirtualTable('hosts'); + + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $this->requireVirtualTable('services'); + + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php new file mode 100644 index 0000000..1492894 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php @@ -0,0 +1,116 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Application\Config; +use Icinga\Data\Filter\FilterExpression; + +class CustomvarQuery extends IdoQuery +{ + protected $columnMap = array( + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'customvariablestatus' => array( + 'varname' => 'cvs.varname', + 'varvalue' => 'cvs.varvalue', + 'is_json' => 'cvs.is_json', + ), + 'objects' => array( + 'host' => 'cvo.name1 COLLATE latin1_general_ci', + 'host_name' => 'cvo.name1', + 'service' => 'cvo.name2 COLLATE latin1_general_ci', + 'service_description' => 'cvo.name2', + 'contact' => 'cvo.name1 COLLATE latin1_general_ci', + 'contact_name' => 'cvo.name1', + 'object_type' => "CASE cvo.objecttype_id WHEN 1 THEN 'host' WHEN 2 THEN 'service' WHEN 10 THEN 'contact' ELSE 'invalid' END", + 'object_type_id' => 'cvo.objecttype_id' +// 'object_type' => "CASE cvo.objecttype_id WHEN 1 THEN 'host' WHEN 2 THEN 'service' WHEN 3 THEN 'hostgroup' WHEN 4 THEN 'servicegroup' WHEN 5 THEN 'hostescalation' WHEN 6 THEN 'serviceescalation' WHEN 7 THEN 'hostdependency' WHEN 8 THEN 'servicedependency' WHEN 9 THEN 'timeperiod' WHEN 10 THEN 'contact' WHEN 11 THEN 'contactgroup' WHEN 12 THEN 'command' ELSE 'other' END" + ), + ); + + public function where($expression, $parameters = null) + { + $types = array('host' => 1, 'service' => 2, 'contact' => 10); + if ($expression === 'object_type') { + parent::where('object_type_id', $types[$parameters]); + } else { + parent::where($expression, $parameters); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $types = ['host' => 1, 'service' => 2, 'contact' => 10]; + if ($ex->getColumn() === 'object_type') { + $ex = clone $ex; + $ex->setColumn('object_type_id'); + $ex->setExpression($types[$ex->getExpression()]); + } + + parent::whereEx($ex); + + return $this; + } + + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.12.0', '<')) { + $this->columnMap['customvariablestatus']['is_json'] = '(0)'; + } + + if (! (bool) Config::module('monitoring')->get('ido', 'use_customvar_status_table', true)) { + $table = 'customvariables'; + } else { + $table = 'customvariablestatus'; + } + + $this->select->from( + array('cvs' => $this->prefix . $table), + array() + )->join( + array('cvo' => $this->prefix . 'objects'), + 'cvs.object_id = cvo.object_id AND cvo.is_active = 1', + array() + ); + $this->joinedVirtualTables = array( + 'customvariablestatus' => true, + 'objects' => true + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = cvs.instance_id', + array() + ); + } + + /** + * {@inheritdoc} + */ + public function getGroup() + { + $group = parent::getGroup(); + if (! empty($group) && $this->ds->getDbType() === 'pgsql') { + foreach ($this->columnMap as $table => $columns) { + $pk = ($table === 'objects' ? 'cvo.' : 'cvs.') . $this->getPrimaryKeyColumn($table); + foreach ($columns as $alias => $_) { + if (! in_array($pk, $group, true) && in_array($alias, $group, true)) { + $group[] = $pk; + break; + } + } + } + } + + return $group; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php new file mode 100644 index 0000000..9bc1d88 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php @@ -0,0 +1,163 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service downtimes + */ +class DowntimeQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimes' => array( + 'downtime_author' => 'd.downtime_author', + 'downtime_author_name' => 'd.downtime_author_name', + 'downtime_comment' => 'd.downtime_comment', + 'downtime_duration' => 'd.downtime_duration', + 'downtime_end' => 'd.downtime_end', + 'downtime_entry_time' => 'd.downtime_entry_time', + 'downtime_internal_id' => 'd.downtime_internal_id', + 'downtime_is_fixed' => 'd.downtime_is_fixed', + 'downtime_is_flexible' => 'd.downtime_is_flexible', + 'downtime_is_in_effect' => 'd.downtime_is_in_effect', + 'downtime_name' => 'd.downtime_name', + 'downtime_scheduled_end' => 'd.downtime_scheduled_end', + 'downtime_scheduled_start' => 'd.downtime_scheduled_start', + 'downtime_start' => 'd.downtime_start', + 'object_type' => 'd.object_type', + 'instance_name' => 'd.instance_name' + ), + 'hosts' => array( + 'host_display_name' => 'd.host_display_name', + 'host_name' => 'd.host_name', + 'host_state' => 'd.host_state' + ), + 'services' => array( + 'service_description' => 'd.service_description', + 'service_display_name' => 'd.service_display_name', + 'service_host_name' => 'd.service_host_name', + 'service_state' => 'd.service_state' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $downtimeQuery; + + /** + * Subqueries used for the downtime query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.14.0', '<')) { + $this->columnMap['downtimes']['downtime_name'] = '(NULL)'; + } + $this->downtimeQuery = $this->db->select(); + $this->select->from( + array('d' => $this->downtimeQuery), + array() + ); + $this->joinedVirtualTables['downtimes'] = true; + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys($this->columnMap['downtimes'] + $this->columnMap['hosts']); + foreach (array_keys($this->columnMap['services']) as $column) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + $hosts = $this->createSubQuery('hostdowntime', $columns); + $this->subQueries[] = $hosts; + $this->downtimeQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys($this->columnMap['downtimes'] + $this->columnMap['hosts'] + $this->columnMap['services']); + $services = $this->createSubQuery('servicedowntime', $columns); + $this->subQueries[] = $services; + $this->downtimeQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php new file mode 100644 index 0000000..de47418 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service downtime end history records + */ +class DowntimeendhistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimehistory' => array( + 'id' => 'deh.id', + 'object_type' => 'deh.object_type' + ), + 'history' => array( + 'type' => 'deh.type', + 'timestamp' => 'deh.timestamp', + 'object_id' => 'deh.object_id', + 'state' => 'deh.state', + 'output' => 'deh.output' + ), + 'hosts' => array( + 'host_display_name' => 'deh.host_display_name', + 'host_name' => 'deh.host_name' + ), + 'services' => array( + 'service_description' => 'deh.service_description', + 'service_display_name' => 'deh.service_display_name', + 'service_host_name' => 'deh.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $downtimeEndHistoryQuery; + + /** + * Subqueries used for the downtime end history query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Whether to additionally select all history columns + * + * @var bool + */ + protected $fetchHistoryColumns = false; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->downtimeEndHistoryQuery = $this->db->select(); + $this->select->from( + array('deh' => $this->downtimeEndHistoryQuery), + array() + ); + $this->joinedVirtualTables['downtimehistory'] = true; + } + + /** + * Join history related columns and tables + */ + protected function joinHistory() + { + // TODO: Ensure that one is selecting the history columns first... + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + $this->requireVirtualTable('services'); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['downtimehistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hostdowntimeendhistory', $columns); + $this->subQueries[] = $hosts; + $this->downtimeEndHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['downtimehistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Servicedowntimeendhistory', $columns); + $this->subQueries[] = $services; + $this->downtimeEndHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php new file mode 100644 index 0000000..04e6aa5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host and service downtime events + */ +class DowntimeeventQuery extends IdoQuery +{ + protected $columnMap = array( + 'downtimeevent' => array( + 'downtimeevent_id' => 'dth.downtimehistory_id', + 'downtimeevent_entry_time' => 'UNIX_TIMESTAMP(dth.entry_time)', + 'downtimeevent_author_name' => 'dth.author_name', + 'downtimeevent_comment_data' => 'dth.comment_data', + 'downtimeevent_is_fixed' => 'dth.is_fixed', + 'downtimeevent_scheduled_start_time' => 'UNIX_TIMESTAMP(dth.scheduled_start_time)', + 'downtimeevent_scheduled_end_time' => 'UNIX_TIMESTAMP(dth.scheduled_end_time)', + 'downtimeevent_was_started' => 'dth.was_started', + 'downtimeevent_actual_start_time' => 'UNIX_TIMESTAMP(dth.actual_start_time)', + 'downtimeevent_actual_end_time' => 'UNIX_TIMESTAMP(dth.actual_end_time)', + 'downtimeevent_was_cancelled' => 'dth.was_cancelled', + 'downtimeevent_is_in_effect' => 'dth.is_in_effect', + 'downtimeevent_trigger_time' => 'UNIX_TIMESTAMP(dth.trigger_time)' + ), + 'object' => array( + 'host_name' => 'o.name1', + 'service_description' => 'o.name2' + ) + ); + + protected function joinBaseTables() + { + $this->select() + ->from(array('dth' => $this->prefix . 'downtimehistory'), array()) + ->join(array('o' => $this->prefix . 'objects'), 'dth.object_id = o.object_id', array()); + + $this->joinedVirtualTables['downtimeevent'] = true; + $this->joinedVirtualTables['object'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php new file mode 100644 index 0000000..3ba600d --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service downtime start history records + */ +class DowntimestarthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimehistory' => array( + 'id' => 'dsh.id', + 'object_type' => 'dsh.object_type' + ), + 'history' => array( + 'type' => 'dsh.type', + 'timestamp' => 'dsh.timestamp', + 'object_id' => 'dsh.object_id', + 'state' => 'dsh.state', + 'output' => 'dsh.output' + ), + 'hosts' => array( + 'host_display_name' => 'dsh.host_display_name', + 'host_name' => 'dsh.host_name' + ), + 'services' => array( + 'service_description' => 'dsh.service_description', + 'service_display_name' => 'dsh.service_display_name', + 'service_host_name' => 'dsh.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $downtimeStartHistoryQuery; + + /** + * Subqueries used for the downtime start history query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Whether to additionally select all history columns + * + * @var bool + */ + protected $fetchHistoryColumns = false; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->downtimeStartHistoryQuery = $this->db->select(); + $this->select->from( + array('dsh' => $this->downtimeStartHistoryQuery), + array() + ); + $this->joinedVirtualTables['downtimehistory'] = true; + } + + /** + * Join history related columns and tables + */ + protected function joinHistory() + { + // TODO: Ensure that one is selecting the history columns first... + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + $this->requireVirtualTable('services'); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['downtimehistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hostdowntimestarthistory', $columns); + $this->subQueries[] = $hosts; + $this->downtimeStartHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['downtimehistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Servicedowntimestarthistory', $columns); + $this->subQueries[] = $services; + $this->downtimeStartHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php new file mode 100644 index 0000000..a99d6b7 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php @@ -0,0 +1,38 @@ +<?php +/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class EmptyhostgroupQuery extends HostgroupQuery +{ + protected $subQueryTargets = []; + + protected $columnMap = [ + 'hostgroups' => [ + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1', + 'host_name' => '(NULL)', + 'service_description' => '(NULL)', + 'servicegroup_name' => '(NULL)', + 'host_contact' => '(NULL)', + 'host_contactgroup' => '(NULL)' + ], + 'instances' => [ + 'instance_name' => 'i.instance_name' + ] + ]; + + protected function joinBaseTables() + { + parent::joinBaseTables(); + + $this->select->joinLeft( + ['ehgm' => $this->prefix . 'hostgroup_members'], + 'ehgm.hostgroup_id = hg.hostgroup_id', + [] + ); + $this->select->group(['hgo.object_id', 'hg.hostgroup_id']); + $this->select->having('COUNT(ehgm.hostgroup_member_id) = ?', 0); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php new file mode 100644 index 0000000..88ee4c3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php @@ -0,0 +1,51 @@ +<?php +/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class EmptyservicegroupQuery extends ServicegroupQuery +{ + protected $subQueryTargets = []; + + protected $columnMap = [ + 'servicegroups' => [ + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'host_name' => '(NULL)', + 'hostgroup_name' => '(NULL)', + 'service_description' => '(NULL)', + 'host_contact' => '(NULL)', + 'host_contactgroup' => '(NULL)', + 'service_contact' => '(NULL)', + 'service_contactgroup' => '(NULL)' + ], + 'instances' => [ + 'instance_name' => 'i.instance_name' + ] + ]; + + protected function joinBaseTables() + { + parent::joinBaseTables(); + + $this->select->joinLeft( + ['esgm' => $this->prefix . 'servicegroup_members'], + 'esgm.servicegroup_id = sg.servicegroup_id', + [] + ); + $this->select->group(['sgo.object_id', 'sg.servicegroup_id']); + $this->select->having('COUNT(esgm.servicegroup_member_id) = ?', 0); + } + + protected function joinHosts() + { + parent::joinHosts(); + + $this->select->joinLeft( + ['h' => 'icinga_hosts'], + 'h.host_object_id = s.host_object_id', + [] + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php new file mode 100644 index 0000000..297b20a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php @@ -0,0 +1,57 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +abstract class EventgridQuery extends StatehistoryQuery +{ + /** + * The columns additionally provided by this query + * + * @var array + */ + protected $additionalColumns = array( + 'day' => 'DATE(FROM_UNIXTIME(sth.timestamp))', + 'cnt_up' => "SUM(CASE WHEN sth.state = 0 THEN 1 ELSE 0 END)", + 'cnt_down_hard' => "SUM(CASE WHEN sth.state = 1 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)", + 'cnt_down' => "SUM(CASE WHEN sth.state = 1 THEN 1 ELSE 0 END)", + 'cnt_unreachable_hard' => "SUM(CASE WHEN sth.state = 2 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)", + 'cnt_unreachable' => "SUM(CASE WHEN sth.state = 2 THEN 1 ELSE 0 END)", + 'cnt_unknown_hard' => "SUM(CASE WHEN sth.state = 3 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)", + 'cnt_unknown' => "SUM(CASE WHEN sth.state = 3 THEN 1 ELSE 0 END)", + 'cnt_unknown_hard' => "SUM(CASE WHEN sth.state = 3 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)", + 'cnt_critical' => "SUM(CASE WHEN sth.state = 2 THEN 1 ELSE 0 END)", + 'cnt_critical_hard' => "SUM(CASE WHEN sth.state = 2 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)", + 'cnt_warning' => "SUM(CASE WHEN sth.state = 1 THEN 1 ELSE 0 END)", + 'cnt_warning_hard' => "SUM(CASE WHEN sth.state = 1 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)", + 'cnt_ok' => "SUM(CASE WHEN sth.state = 0 THEN 1 ELSE 0 END)" + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + parent::joinBaseTables(); + $this->requireVirtualTable('history'); + $this->columnMap['statehistory'] += $this->additionalColumns; + $this->select->group(array('DATE(FROM_UNIXTIME(sth.timestamp))')); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + if (array_key_exists($columnOrAlias, $this->additionalColumns)) { + $subQueries = $this->subQueries; + $this->subQueries = array(); + parent::order($columnOrAlias, $dir); + $this->subQueries = $subQueries; + } else { + parent::order($columnOrAlias, $dir); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php new file mode 100644 index 0000000..62d92e4 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php @@ -0,0 +1,16 @@ +<?php + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class EventgridhostsQuery extends EventgridQuery +{ + + /** + * Join history related columns and tables, hosts only + */ + protected function joinHistory() + { + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php new file mode 100644 index 0000000..424de45 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class EventgridservicesQuery extends EventgridQuery +{ + /** + * Join history related columns and tables, services only + */ + protected function joinHistory() + { + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('services'); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php new file mode 100644 index 0000000..680e2ca --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php @@ -0,0 +1,134 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for event history records + */ +class EventhistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $useSubqueryCount = true; + + /** + * Subqueries used for the event history query + * + * @type IdoQuery[] + */ + protected $subQueries = array(); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'eventhistory' => array( + 'id' => 'eh.id', + 'host_name' => 'eh.host_name', + 'service_description' => 'eh.service_description', + 'object_type' => 'eh.object_type', + 'timestamp' => 'eh.timestamp', + 'state' => 'eh.state', + 'output' => 'eh.output', + 'type' => 'eh.type', + 'host_display_name' => 'eh.host_display_name', + 'service_display_name' => 'eh.service_display_name' + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $columns = array( + 'id', + 'timestamp', + 'output', + 'type', + 'state', + 'object_type', + 'host_name', + 'service_description', + 'host_display_name', + 'service_display_name' + ); + $this->subQueries = array( + $this->createSubQuery('Notificationhistory', $columns), + $this->createSubQuery('Statehistory', $columns), + $this->createSubQuery('Downtimestarthistory', $columns), + $this->createSubQuery('Downtimeendhistory', $columns), + $this->createSubQuery('Commenthistory', $columns), + $this->createSubQuery('Commentdeletionhistory', $columns), + $this->createSubQuery('Flappingstarthistory', $columns), + $this->createSubQuery('Flappingendhistory', $columns) + ); + $sub = $this->db->select()->union($this->subQueries, Zend_Db_Select::SQL_UNION_ALL); + $this->select->from(array('eh' => $sub), array()); + $this->joinedVirtualTables['eventhistory'] = true; + } + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php new file mode 100644 index 0000000..7bdf332 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php @@ -0,0 +1,49 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service flapping end history records + */ +class FlappingendhistoryQuery extends FlappingstarthistoryQuery +{ + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['flappinghistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hostflappingendhistory', $columns); + $this->subQueries[] = $hosts; + $this->flappingStartHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['flappinghistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Serviceflappingendhistory', $columns); + $this->subQueries[] = $services; + $this->flappingStartHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php new file mode 100644 index 0000000..d993467 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php @@ -0,0 +1,36 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host and service flapping events + */ +class FlappingeventQuery extends IdoQuery +{ + protected $columnMap = array( + 'flappingevent' => array( + 'flappingevent_id' => 'fh.flappinghistory_id', + 'flappingevent_event_time' => 'UNIX_TIMESTAMP(fh.event_time)', + 'flappingevent_event_type' => "(CASE fh.event_type WHEN 1000 THEN 'flapping' WHEN 1001 THEN 'flapping_deleted' ELSE NULL END)", + 'flappingevent_reason_type' => "(CASE fh.reason_type WHEN 1 THEN 'stopped' WHEN 2 THEN 'disabled' ELSE NULL END)", + 'flappingevent_percent_state_change' => 'fh.percent_state_change', + 'flappingevent_low_threshold' => 'fh.low_threshold', + 'flappingevent_high_threshold' => 'fh.high_threshold' + ), + 'object' => array( + 'host_name' => 'o.name1', + 'service_description' => 'o.name2' + ) + ); + + protected function joinBaseTables() + { + $this->select() + ->from(array('fh' => $this->prefix . 'flappinghistory'), array()) + ->join(array('o' => $this->prefix . 'objects'), 'fh.object_id = o.object_id', array()); + + $this->joinedVirtualTables['flappingevent'] = true; + $this->joinedVirtualTables['object'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php new file mode 100644 index 0000000..5c8bec5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service flapping start history records + */ +class FlappingstarthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'flappinghistory' => array( + 'id' => 'fsh.id', + 'object_type' => 'fsh.object_type' + ), + 'history' => array( + 'type' => 'fsh.type', + 'timestamp' => 'fsh.timestamp', + 'object_id' => 'fsh.object_id', + 'state' => 'fsh.state', + 'output' => 'fsh.output' + ), + 'hosts' => array( + 'host_display_name' => 'fsh.host_display_name', + 'host_name' => 'fsh.host_name' + ), + 'services' => array( + 'service_description' => 'fsh.service_description', + 'service_display_name' => 'fsh.service_display_name', + 'service_host_name' => 'fsh.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $flappingStartHistoryQuery; + + /** + * Subqueries used for the flapping start history query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Whether to additionally select all history columns + * + * @var bool + */ + protected $fetchHistoryColumns = false; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->flappingStartHistoryQuery = $this->db->select(); + $this->select->from( + array('fsh' => $this->flappingStartHistoryQuery), + array() + ); + $this->joinedVirtualTables['flappinghistory'] = true; + } + + /** + * Join history related columns and tables + */ + protected function joinHistory() + { + // TODO: Ensure that one is selecting the history columns first... + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + $this->requireVirtualTable('services'); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['flappinghistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hostflappingstarthistory', $columns); + $this->subQueries[] = $hosts; + $this->flappingStartHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['flappinghistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Serviceflappingstarthistory', $columns); + $this->subQueries[] = $services; + $this->flappingStartHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php new file mode 100644 index 0000000..60ea5ef --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Zend_Db_Select; + +/** + * Query for host and service group summaries + */ +class GroupsummaryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'hoststatussummary' => array( + 'hostgroup' => 'hostgroup COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hostgroup_alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hostgroup_name', + 'hosts_up' => 'SUM(CASE WHEN object_type = \'host\' AND state = 0 THEN 1 ELSE 0 END)', + 'hosts_unreachable' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 THEN 1 ELSE 0 END)', + 'hosts_unreachable_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime != 0 THEN 1 ELSE 0 END)', + 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime = 0 THEN 1 ELSE 0 END)', + 'hosts_down' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 THEN 1 ELSE 0 END)', + 'hosts_down_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime != 0 THEN 1 ELSE 0 END)', + 'hosts_down_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime != 0 THEN state_change ELSE 0 END)', + 'hosts_down_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime = 0 THEN state_change ELSE 0 END)', + 'hosts_down_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime = 0 THEN 1 ELSE 0 END)', + 'hosts_pending' => 'SUM(CASE WHEN object_type = \'host\' AND state = 99 THEN 1 ELSE 0 END)', + 'hosts_pending_last_state_change' => 'MAX(CASE WHEN object_type = \'host\' AND state = 99 THEN state_change ELSE 0 END)', + 'hosts_severity' => 'MAX(CASE WHEN object_type = \'host\' THEN severity ELSE 0 END)', + 'hosts_total' => 'SUM(CASE WHEN object_type = \'host\' THEN 1 ELSE 0 END)', + 'hosts_unreachable_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime != 0 THEN state_change ELSE 0 END)', + 'hosts_unreachable_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime = 0 THEN state_change ELSE 0 END)', + 'hosts_up_last_state_change' => 'MAX(CASE WHEN object_type = \'host\' AND state = 0 THEN state_change ELSE 0 END)' + ), + 'servicestatussummary' => array( + 'servicegroup' => 'servicegroup COLLATE latin1_general_ci', + 'servicegroup_alias' => 'servicegroup_alias COLLATE latin1_general_ci', + 'servicegroup_name' => 'servicegroup_name', + 'services_critical' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 THEN 1 ELSE 0 END)', + 'services_critical_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state > 0 THEN 1 ELSE 0 END)', + 'services_critical_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state > 0 THEN state_change ELSE 0 END)', + 'services_critical_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state = 0 THEN state_change ELSE 0 END)', + 'services_critical_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state = 0 THEN 1 ELSE 0 END)', + 'services_ok' => 'SUM(CASE WHEN object_type = \'service\' AND state = 0 THEN 1 ELSE 0 END)', + 'services_ok_last_state_change' => 'MAX(CASE WHEN object_type = \'service\' AND state = 0 THEN state_change ELSE 0 END)', + 'services_pending' => 'SUM(CASE WHEN object_type = \'service\' AND state = 99 THEN 1 ELSE 0 END)', + 'services_pending_last_state_change' => 'MAX(CASE WHEN object_type = \'service\' AND state = 99 THEN state_change ELSE 0 END)', + 'services_severity' => 'MAX(CASE WHEN object_type = \'service\' THEN severity ELSE 0 END)', + 'services_total' => 'SUM(CASE WHEN object_type = \'service\' THEN 1 ELSE 0 END)', + 'services_unknown' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 THEN 1 ELSE 0 END)', + 'services_unknown_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state > 0 THEN 1 ELSE 0 END)', + 'services_unknown_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state > 0 THEN state_change ELSE 0 END)', + 'services_unknown_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state = 0 THEN state_change ELSE 0 END)', + 'services_unknown_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state = 0 THEN 1 ELSE 0 END)', + 'services_warning' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 THEN 1 ELSE 0 END)', + 'services_warning_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state > 0 THEN 1 ELSE 0 END)', + 'services_warning_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state > 0 THEN state_change ELSE 0 END)', + 'services_warning_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state = 0 THEN state_change ELSE 0 END)', + 'services_warning_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state = 0 THEN 1 ELSE 0 END)' + ) + ); + + /** + * {@inheritdoc} + */ + protected $useSubqueryCount = true; + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $columns = array( + 'object_type', + 'host_state' + ); + + if (in_array('servicegroup', $this->desiredColumns) || in_array('servicegroup_name', $this->desiredColumns)) { + $columns[] = 'servicegroup'; + $columns[] = 'servicegroup_name'; + $columns[] = 'servicegroup_alias'; + $groupColumns = array('servicegroup_name', 'servicegroup_alias'); + } else { + $columns[] = 'hostgroup'; + $columns[] = 'hostgroup_name'; + $columns[] = 'hostgroup_alias'; + $groupColumns = array('hostgroup_name', 'hostgroup_alias'); + } + $hosts = $this->createSubQuery( + 'Hoststatus', + $columns + array( + 'state' => 'host_state', + 'acknowledged' => 'host_acknowledged', + 'in_downtime' => 'host_in_downtime', + 'state_change' => 'host_last_state_change', + 'severity' => 'host_severity' + ) + ); + if (in_array('servicegroup_name', $this->desiredColumns)) { + $hosts->group(array( + 'sgo.name1', + 'ho.object_id', + 'sg.alias', + 'state', + 'acknowledged', + 'in_downtime', + 'state_change', + 'severity' + )); + } + $services = $this->createSubQuery( + 'Status', + $columns + array( + 'state' => 'service_state', + 'acknowledged' => 'service_acknowledged', + 'in_downtime' => 'service_in_downtime', + 'state_change' => 'service_last_state_change', + 'severity' => 'service_severity' + ) + ); + $union = $this->db->select()->union(array($hosts, $services), Zend_Db_Select::SQL_UNION_ALL); + $this->select->from(array('statussummary' => $union), array())->group($groupColumns); + $this->joinedVirtualTables = array( + 'servicestatussummary' => true, + 'hoststatussummary' => true + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php new file mode 100644 index 0000000..b388204 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php @@ -0,0 +1,202 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host comments + */ +class HostcommentQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('comments' => array('c.comment_id', 'ho.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'comments' => array( + 'comment_author' => 'c.author_name COLLATE latin1_general_ci', + 'comment_author_name' => 'c.author_name', + 'comment_data' => 'c.comment_data', + 'comment_expiration' => 'CASE c.expires WHEN 1 THEN UNIX_TIMESTAMP(c.expiration_time) ELSE NULL END', + 'comment_internal_id' => 'c.internal_comment_id', + 'comment_is_persistent' => 'c.is_persistent', + 'comment_name' => 'c.name', + 'comment_timestamp' => 'UNIX_TIMESTAMP(c.comment_time)', + 'comment_type' => "CASE c.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'downtime' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END", + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'object_type' => '(\'host\')' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'hoststatus' => array( + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.14.0', '<')) { + $this->columnMap['comments']['comment_name'] = '(NULL)'; + } + $this->select->from( + array('c' => $this->prefix . 'comments'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = c.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + $this->joinedVirtualTables['comments'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = c.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sg.' . $this->servicegroup_id, + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php new file mode 100644 index 0000000..d798d56 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php @@ -0,0 +1,44 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host comment removal records + */ +class HostcommentdeletionhistoryQuery extends HostcommenthistoryQuery +{ + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('hch.deletion_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + parent::joinBaseTables(); + $this->select->where("hch.deletion_time > '1970-01-02 00:00:00'"); + $this->columnMap['commenthistory']['timestamp'] = str_replace( + 'comment_time', + 'deletion_time', + $this->columnMap['commenthistory']['timestamp'] + ); + $this->columnMap['commenthistory']['type'] = str_replace( + 'END)', + "END || '_deleted')", + $this->columnMap['commenthistory']['type'] + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php new file mode 100644 index 0000000..b8f166a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php @@ -0,0 +1,197 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host comment history records + */ +class HostcommenthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('commenthistory' => array('hch.commenthistory_id', 'ho.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'commenthistory' => array( + 'id' => 'hch.commenthistory_id', + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'object_id' => 'hch.object_id', + 'object_type' => '(\'host\')', + 'output' => "('[' || hch.author_name || '] ' || hch.comment_data)", + 'state' => '(-1)', + 'timestamp' => 'UNIX_TIMESTAMP(hch.comment_time)', + 'type' => "(CASE hch.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'dt_comment' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END)" + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('hch.comment_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('hch' => $this->prefix . 'commenthistory'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = hch.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + $this->joinedVirtualTables['commenthistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = hch.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php new file mode 100644 index 0000000..23b0e90 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php @@ -0,0 +1,247 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host contacts + */ +class HostcontactQuery extends IdoQuery +{ + protected $allowCustomVars = true; + + protected $groupBase = [ + 'contacts' => ['co.object_id', 'c.contact_id'], + 'timeperiods' => ['ht.timeperiod_id', 'st.timeperiod_id'] + ]; + + protected $groupOrigin = ['contactgroups', 'hosts', 'services']; + + protected $subQueryTargets = [ + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ]; + + protected $columnMap = [ + 'contactgroups' => [ + 'contactgroup' => 'cgo.name1 COLLATE latin1_general_ci', + 'contactgroup_name' => 'cgo.name1', + 'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci' + ], + 'contacts' => [ + 'contact_id' => 'c.contact_id', + 'contact' => 'co.name1 COLLATE latin1_general_ci', + 'contact_name' => 'co.name1', + 'contact_alias' => 'c.alias COLLATE latin1_general_ci', + 'contact_email' => 'c.email_address COLLATE latin1_general_ci', + 'contact_pager' => 'c.pager_address', + 'contact_object_id' => 'c.contact_object_id', + 'contact_has_host_notfications' => 'c.host_notifications_enabled', + 'contact_has_service_notfications' => 'c.service_notifications_enabled', + 'contact_can_submit_commands' => 'c.can_submit_commands', + 'contact_notify_service_recovery' => 'c.notify_service_recovery', + 'contact_notify_service_warning' => 'c.notify_service_warning', + 'contact_notify_service_critical' => 'c.notify_service_critical', + 'contact_notify_service_unknown' => 'c.notify_service_unknown', + 'contact_notify_service_flapping' => 'c.notify_service_flapping', + 'contact_notify_service_downtime' => 'c.notify_service_downtime', + 'contact_notify_host_recovery' => 'c.notify_host_recovery', + 'contact_notify_host_down' => 'c.notify_host_down', + 'contact_notify_host_unreachable' => 'c.notify_host_unreachable', + 'contact_notify_host_flapping' => 'c.notify_host_flapping', + 'contact_notify_host_downtime' => 'c.notify_host_downtime' + ], + 'hostgroups' => [ + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ], + 'hosts' => [ + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ], + 'instances' => [ + 'instance_name' => 'i.instance_name' + ], + 'servicegroups' => [ + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ], + 'services' => [ + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ], + 'timeperiods' => [ + 'contact_notify_host_timeperiod' => 'ht.alias COLLATE latin1_general_ci', + 'contact_notify_service_timeperiod' => 'st.alias COLLATE latin1_general_ci' + ] + ]; + + protected function joinBaseTables() + { + $this->select->from( + ['c' => $this->prefix . 'contacts'], + [] + )->join( + ['co' => $this->prefix . 'objects'], + 'co.object_id = c.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10', + [] + ); + + $this->joinedVirtualTables = array('contacts' => true); + } + + /** + * Join contact groups + */ + protected function joinContactgroups() + { + $this->select->joinLeft( + ['cgm' => $this->prefix . 'contactgroup_members'], + 'co.object_id = cgm.contact_object_id', + [] + )->joinLeft( + ['cg' => $this->prefix . 'contactgroups'], + 'cgm.contactgroup_id = cg.contactgroup_id', + [] + )->joinLeft( + ['cgo' => $this->prefix . 'objects'], + 'cg.contactgroup_object_id = cgo.object_id AND cgo.is_active = 1 AND cgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('hosts'); + + $this->select->joinLeft( + ['hgm' => $this->prefix . 'hostgroup_members'], + 'hgm.host_object_id = ho.object_id', + [] + )->joinLeft( + ['hg' => $this->prefix . 'hostgroups'], + 'hg.hostgroup_id = hgm.hostgroup_id', + [] + )->joinLeft( + ['hgo' => $this->prefix . 'objects'], + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + [] + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->joinLeft( + ['hc' => $this->prefix . 'host_contacts'], + 'hc.contact_object_id = c.contact_object_id', + [] + )->joinLeft( + ['h' => $this->prefix . 'hosts'], + 'h.host_id = hc.host_id', + [] + )->joinLeft( + ['ho' => $this->prefix . 'objects'], + 'ho.object_id = h.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + [] + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + ['i' => $this->prefix . 'instances'], + 'i.instance_id = c.instance_id', + [] + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + ['sgm' => $this->prefix . 'servicegroup_members'], + 'sgm.service_object_id = s.service_object_id', + [] + )->joinLeft( + ['sg' => $this->prefix . 'servicegroups'], + 'sg.servicegroup_id = sgm.servicegroup_id', + [] + )->joinLeft( + ['sgo' => $this->prefix . 'objects'], + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + [] + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->requireVirtualTable('hosts'); + + $this->select->joinLeft( + ['s' => $this->prefix . 'services'], + 's.host_object_id = ho.object_id', + [] + )->joinLeft( + ['so' => $this->prefix . 'objects'], + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + [] + ); + } + + /** + * Join time periods + */ + protected function joinTimeperiods() + { + $this->select->joinLeft( + ['ht' => $this->prefix . 'timeperiods'], + 'ht.timeperiod_object_id = c.host_timeperiod_object_id', + [] + ); + $this->select->joinLeft( + ['st' => $this->prefix . 'timeperiods'], + 'st.timeperiod_object_id = c.service_timeperiod_object_id', + [] + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $this->requireVirtualTable('hosts'); + + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $this->requireVirtualTable('services'); + + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php new file mode 100644 index 0000000..62f5ceb --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php @@ -0,0 +1,208 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host downtimes + */ +class HostdowntimeQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('downtimes' => array('sd.scheduleddowntime_id', 'ho.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimes' => array( + 'downtime_author' => 'sd.author_name COLLATE latin1_general_ci', + 'downtime_author_name' => 'sd.author_name', + 'downtime_comment' => 'sd.comment_data', + 'downtime_duration' => 'sd.duration', + 'downtime_end' => 'CASE WHEN sd.is_fixed > 0 THEN UNIX_TIMESTAMP(sd.scheduled_end_time) ELSE UNIX_TIMESTAMP(sd.trigger_time) + sd.duration END', + 'downtime_entry_time' => 'UNIX_TIMESTAMP(sd.entry_time)', + 'downtime_internal_id' => 'sd.internal_downtime_id', + 'downtime_is_fixed' => 'sd.is_fixed', + 'downtime_is_flexible' => 'CASE WHEN sd.is_fixed = 0 THEN 1 ELSE 0 END', + 'downtime_is_in_effect' => 'sd.is_in_effect', + 'downtime_name' => 'sd.name', + 'downtime_scheduled_end' => 'UNIX_TIMESTAMP(sd.scheduled_end_time)', + 'downtime_scheduled_start' => 'UNIX_TIMESTAMP(sd.scheduled_start_time)', + 'downtime_start' => 'UNIX_TIMESTAMP(CASE WHEN UNIX_TIMESTAMP(sd.trigger_time) > 0 then sd.trigger_time ELSE sd.scheduled_start_time END)', + 'downtime_triggered_by_id' => 'sd.triggered_by_id', + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'object_type' => '(\'host\')' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'hoststatus' => array( + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.14.0', '<')) { + $this->columnMap['downtimes']['downtime_name'] = '(NULL)'; + } + $this->select->from( + array('sd' => $this->prefix . 'scheduleddowntime'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'sd.object_id = ho.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + $this->joinedVirtualTables['downtimes'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sd.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sg.' . $this->servicegroup_id, + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php new file mode 100644 index 0000000..77d91e5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host downtime end history records + */ +class HostdowntimeendhistoryQuery extends HostdowntimestarthistoryQuery +{ + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('hdh.actual_end_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + parent::joinBaseTables(true); + $this->select->where("hdh.actual_end_time > '1970-01-02 00:00:00'"); + $this->columnMap['downtimehistory']['type'] = "('dt_end')"; + $this->columnMap['downtimehistory']['timestamp'] = str_replace( + 'actual_start_time', + 'actual_end_time', + $this->columnMap['downtimehistory']['timestamp'] + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php new file mode 100644 index 0000000..54ac6a1 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php @@ -0,0 +1,204 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host downtime start history records + */ +class HostdowntimestarthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('downtimehistory' => array('hdh.downtimehistory_id', 'ho.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimehistory' => array( + 'id' => 'hdh.downtimehistory_id', + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'object_id' => 'hdh.object_id', + 'object_type' => '(\'host\')', + 'output' => "('[' || hdh.author_name || '] ' || hdh.comment_data)", + 'state' => '(-1)', + 'timestamp' => 'UNIX_TIMESTAMP(hdh.actual_start_time)', + 'type' => "('dt_start')" + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('hdh.actual_start_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('hdh' => $this->prefix . 'downtimehistory'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = hdh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + + if (func_num_args() === 0 || func_get_arg(0) === false) { + $this->select->where( + "hdh.actual_start_time > '1970-01-02 00:00:00'" + ); + } + + $this->joinedVirtualTables['downtimehistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = hdh.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php new file mode 100644 index 0000000..ebc346b --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host flapping end history records + */ +class HostflappingendhistoryQuery extends HostflappingstarthistoryQuery +{ + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('hfh' => $this->prefix . 'flappinghistory'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = hfh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + + $this->select->where('hfh.event_type = 1001'); + + $this->joinedVirtualTables['flappinghistory'] = true; + + $this->columnMap['flappinghistory']['type'] = '(\'flapping_deleted\')'; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php new file mode 100644 index 0000000..497a493 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php @@ -0,0 +1,200 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host flapping start history records + */ +class HostflappingstarthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('flappinghistory' => array('hfh.flappinghistory_id', 'ho.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'flappinghistory' => array( + 'id' => 'hfh.flappinghistory_id', + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'object_id' => 'hfh.object_id', + 'object_type' => '(\'host\')', + 'output' => '(hfh.percent_state_change || \'\')', + 'state' => '(-1)', + 'timestamp' => 'UNIX_TIMESTAMP(hfh.event_time)', + 'type' => '(\'flapping\')' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup_name' => 'sgo.name1', + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('hfh.event_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('hfh' => $this->prefix . 'flappinghistory'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = hfh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + + $this->select->where('hfh.event_type = 1000'); + + $this->joinedVirtualTables['flappinghistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = hfh.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php new file mode 100644 index 0000000..463fba9 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php @@ -0,0 +1,295 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host groups + */ +class HostgroupQuery extends IdoQuery +{ + protected $allowCustomVars = true; + + protected $groupBase = array( + 'hostgroups' => array('hgo.object_id', 'hg.hostgroup_id'), + 'hoststatus' => array('hs.hoststatus_id'), + 'servicestatus' => array('ss.servicestatus_id') + ); + + protected $groupOrigin = array('members'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + protected $columnMap = array( + 'contacts' => [ + 'host_contact' => 'hco.name1' + ], + 'contactgroups' => [ + 'host_contactgroup' => 'hcgo.name1' + ], + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hoststatus' => array( + 'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END', + 'host_severity' => ' + CASE + WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL + THEN 16 + ELSE + CASE + WHEN hs.current_state = 0 + THEN 1 + ELSE + CASE + WHEN hs.current_state = 1 THEN 64 + WHEN hs.current_state = 2 THEN 32 + ELSE 256 + END + + + CASE + WHEN hs.problem_has_been_acknowledged = 1 THEN 2 + WHEN hs.scheduled_downtime_depth > 0 THEN 1 + ELSE 256 + END + END + END', + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'members' => array( + 'host_name' => 'ho.name1' + ), + 'servicegroups' => array( + 'servicegroup_name' => 'sgo.name1' + ), + 'services' => array( + 'service_description' => 'so.name2' + ), + 'servicestatus' => array( + 'service_handled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) > 0 THEN 1 ELSE 0 END', + 'service_severity' => ' + CASE WHEN ss.current_state = 0 + THEN + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL + THEN 16 + ELSE 0 + END + + + CASE WHEN ss.problem_has_been_acknowledged = 1 + THEN 2 + ELSE + CASE WHEN ss.scheduled_downtime_depth > 0 + THEN 1 + ELSE 4 + END + END + ELSE + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 16 + WHEN ss.current_state = 1 THEN 32 + WHEN ss.current_state = 2 THEN 128 + WHEN ss.current_state = 3 THEN 64 + ELSE 256 + END + + + CASE WHEN hs.current_state > 0 + THEN 1024 + ELSE + CASE WHEN ss.problem_has_been_acknowledged = 1 + THEN 512 + ELSE + CASE WHEN ss.scheduled_downtime_depth > 0 + THEN 256 + ELSE 2048 + END + END + END + END', + 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END' + ) + ); + + protected function joinBaseTables() + { + $this->select->from( + array('hgo' => $this->prefix . 'objects'), + array() + )->join( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_object_id = hgo.object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + $this->joinedVirtualTables['hostgroups'] = true; + } + + /** + * Join contacts + */ + protected function joinContacts() + { + $this->requireVirtualTable('hosts'); + + $this->select->joinLeft( + ['hc' => 'icinga_host_contacts'], + 'hc.host_id = h.host_id', + [] + )->joinLeft( + ['hco' => 'icinga_objects'], + 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10', + [] + ); + } + + /** + * Join contact groups + */ + protected function joinContactgroups() + { + $this->requireVirtualTable('hosts'); + + $this->select->joinLeft( + ['hcg' => 'icinga_host_contactgroups'], + 'hcg.host_id = h.host_id', + [] + )->joinLeft( + ['hcgo' => 'icinga_objects'], + 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11', + [] + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('members'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->requireVirtualTable('members'); + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = hg.instance_id', + array() + ); + } + + /** + * Join members + */ + protected function joinMembers() + { + $this->select->join( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.hostgroup_id = hg.hostgroup_id', + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'hgm.host_object_id = ho.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sg.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->requireVirtualTable('hosts'); + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = h.host_object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + /** + * Join service status + */ + protected function joinServicestatus() + { + $this->requireVirtualTable('services'); + $this->requireVirtualTable('hoststatus'); + $this->select->join( + array('ss' => $this->prefix . 'servicestatus'), + 'ss.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + // Propagate that the "parent" query has to be filtered as well + $additionalFilter = clone $filter; + + $this->requireVirtualTable('members'); + + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $this->requireVirtualTable('members'); + + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php new file mode 100644 index 0000000..a1b7182 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php @@ -0,0 +1,142 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Zend_Db_Expr; +use Zend_Db_Select; + +use Icinga\Data\Filter\Filter; + +/** + * Query for host group summary + */ +class HostgroupsummaryQuery extends IdoQuery +{ + protected $allowCustomVars = true; + + protected $columnMap = array( + 'hostgroupsummary' => array( + 'hostgroup_alias' => 'hostgroup_alias', + 'hostgroup_name' => 'hostgroup_name', + 'hosts_down' => 'SUM(CASE WHEN host_state = 1 THEN 1 ELSE 0 END)', + 'hosts_down_handled' => 'SUM(CASE WHEN host_state = 1 AND host_handled = 1 THEN 1 ELSE 0 END)', + 'hosts_down_unhandled' => 'SUM(CASE WHEN host_state = 1 AND host_handled = 0 THEN 1 ELSE 0 END)', + 'hosts_pending' => 'SUM(CASE WHEN host_state = 99 THEN 1 ELSE 0 END)', + 'hosts_severity' => 'MAX(host_severity)', + 'hosts_total' => 'SUM(CASE WHEN host_state IS NOT NULL THEN 1 ELSE 0 END)', + 'hosts_unreachable' => 'SUM(CASE WHEN host_state = 2 THEN 1 ELSE 0 END)', + 'hosts_unreachable_handled' => 'SUM(CASE WHEN host_state = 2 AND host_handled = 1 THEN 1 ELSE 0 END)', + 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN host_state = 2 AND host_handled = 0 THEN 1 ELSE 0 END)', + 'hosts_up' => 'SUM(CASE WHEN host_state = 0 THEN 1 ELSE 0 END)', + 'services_critical' => 'SUM(CASE WHEN service_state = 2 THEN 1 ELSE 0 END)', + 'services_critical_handled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 1 THEN 1 ELSE 0 END)', + 'services_critical_unhandled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 0 THEN 1 ELSE 0 END)', + 'services_ok' => 'SUM(CASE WHEN service_state = 0 THEN 1 ELSE 0 END)', + 'services_pending' => 'SUM(CASE WHEN service_state = 99 THEN 1 ELSE 0 END)', + 'services_total' => 'SUM(CASE WHEN service_state IS NOT NULL THEN 1 ELSE 0 END)', + 'services_unknown' => 'SUM(CASE WHEN service_state = 3 THEN 1 ELSE 0 END)', + 'services_unknown_handled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 1 THEN 1 ELSE 0 END)', + 'services_unknown_unhandled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 0 THEN 1 ELSE 0 END)', + 'services_warning' => 'SUM(CASE WHEN service_state = 1 THEN 1 ELSE 0 END)', + 'services_warning_handled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 1 THEN 1 ELSE 0 END)', + 'services_warning_unhandled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 0 THEN 1 ELSE 0 END)', + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $summaryQuery; + + /** + * Subqueries used for the summary query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Count query + * + * @var IdoQuery + */ + protected $countQuery; + + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + $this->countQuery->applyFilter(clone $filter); + return $this; + } + + protected function joinBaseTables() + { + $this->countQuery = $this->createSubQuery( + 'Hostgroup', + array() + ); + $hosts = $this->createSubQuery( + 'Hostgroup', + array( + 'hostgroup_alias', + 'hostgroup_name', + 'host_handled', + 'host_severity', + 'host_state', + 'service_handled' => new Zend_Db_Expr('NULL'), + 'service_severity' => new Zend_Db_Expr('0'), + 'service_state' => new Zend_Db_Expr('NULL'), + ) + ); + $this->subQueries[] = $hosts; + $services = $this->createSubQuery( + 'Hostgroup', + array( + 'hostgroup_alias', + 'hostgroup_name', + 'host_handled' => new Zend_Db_Expr('NULL'), + 'host_severity' => new Zend_Db_Expr('0'), + 'host_state' => new Zend_Db_Expr('NULL'), + 'service_handled', + 'service_severity', + 'service_state' + ) + ); + $this->subQueries[] = $services; + $emptyGroups = $this->createSubQuery( + 'Emptyhostgroup', + [ + 'hostgroup_alias', + 'hostgroup_name', + 'host_handled' => new Zend_Db_Expr('NULL'), + 'host_severity' => new Zend_Db_Expr('0'), + 'host_state' => new Zend_Db_Expr('NULL'), + 'service_handled' => new Zend_Db_Expr('NULL'), + 'service_severity' => new Zend_Db_Expr('0'), + 'service_state' => new Zend_Db_Expr('NULL'), + ] + ); + $this->subQueries[] = $emptyGroups; + $this->summaryQuery = $this->db->select()->union( + [$hosts, $services, $emptyGroups], + Zend_Db_Select::SQL_UNION_ALL + ); + $this->select->from(array('hostgroupsummary' => $this->summaryQuery), array()); + $this->group(array('hostgroup_name', 'hostgroup_alias')); + $this->joinedVirtualTables['hostgroupsummary'] = true; + } + + public function getCountQuery() + { + $count = $this->countQuery->select(); + $this->countQuery->applyFilterSql($count); + $count->columns(array('hgo.object_id')); + $count->group(array('hgo.object_id')); + return $this->db->select()->from($count, array('cnt' => 'COUNT(*)')); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php new file mode 100644 index 0000000..284468e --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php @@ -0,0 +1,283 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host notifications + */ +class HostnotificationQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'contactnotifications' => array( + 'notification_contact_name' => 'co.name1' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci', + 'host_alias' => 'h.alias COLLATE latin1_general_ci', + ), + 'history' => array( + 'output' => null, + 'state' => 'hn.state', + 'timestamp' => 'UNIX_TIMESTAMP(hn.start_time)', + 'type' => ' + CASE hn.notification_reason + WHEN 1 THEN \'notification_ack\' + WHEN 2 THEN \'notification_flapping\' + WHEN 3 THEN \'notification_flapping_end\' + WHEN 5 THEN \'notification_dt_start\' + WHEN 6 THEN \'notification_dt_end\' + WHEN 7 THEN \'notification_dt_end\' + WHEN 8 THEN \'notification_custom\' + ELSE \'notification_state\' + END', + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'notifications' => array( + 'id' => 'hn.notification_id', + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'notification_output' => 'hn.output', + 'notification_reason' => 'hn.notification_reason', + 'notification_state' => 'hn.state', + 'notification_timestamp' => 'UNIX_TIMESTAMP(hn.start_time)', + 'object_type' => '(\'host\')' + ), + 'servicegroups' => array( + 'servicegroup_name' => 'sgo.name1', + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression) { + switch ($filter->getColumn()) { + case 'output': + $this->requireColumn('output'); + $filter->setColumn('hn.output'); + return null; + case 'timestamp': + case 'notification_timestamp': + $this->requireColumn($filter->getColumn()); + $filter->setColumn('hn.start_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + switch ($this->ds->getDbType()) { + case 'mysql': + $concattedContacts = "GROUP_CONCAT(" + . "DISTINCT co.name1 ORDER BY co.name1 SEPARATOR ', '" + . ") COLLATE latin1_general_ci"; + break; + case 'pgsql': + // TODO: Find a way to order the contact alias list: + $concattedContacts = "ARRAY_TO_STRING(ARRAY_AGG(DISTINCT co.name1), ', ')"; + break; + } + $this->columnMap['history']['output'] = "('[' || $concattedContacts || '] ' || hn.output)"; + + $this->select->from( + array('hn' => $this->prefix . 'notifications'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = hn.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + $this->joinedVirtualTables['notifications'] = true; + } + + /** + * Join virtual table history + */ + protected function joinHistory() + { + $this->requireVirtualTable('contactnotifications'); + } + + /** + * Join contact notifications + */ + protected function joinContactnotifications() + { + $this->select->joinLeft( + array('cn' => $this->prefix . 'contactnotifications'), + 'cn.notification_id = hn.notification_id', + array() + ); + $this->select->joinLeft( + array('co' => $this->prefix . 'objects'), + 'co.object_id = cn.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10', + array() + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = hn.instance_id', + array() + ); + } + + /** + * {@inheritdoc} + */ + public function getGroup() + { + $group = array(); + + if ($this->hasJoinedVirtualTable('history') + || $this->hasJoinedVirtualTable('services') + || $this->hasJoinedVirtualTable('hostgroups') + ) { + $group = array('hn.notification_id', 'ho.object_id'); + if ($this->hasJoinedVirtualTable('contactnotifications') && !$this->hasJoinedVirtualTable('history')) { + $group[] = 'co.object_id'; + } + } elseif ($this->hasJoinedVirtualTable('contactnotifications')) { + $group = array('hn.notification_id', 'co.object_id', 'ho.object_id'); + } + + if (! empty($group)) { + if ($this->hasJoinedVirtualTable('hosts')) { + $group[] = 'h.host_id'; + } + + if ($this->hasJoinedVirtualTable('instances')) { + $group[] = 'i.instance_id'; + } + } + + return $group; + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php new file mode 100644 index 0000000..ac85c1f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php @@ -0,0 +1,222 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host state history records + */ +class HoststatehistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('statehistory' => array('hh.statehistory_id', 'ho.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'services'); + + /** + * Array to map type names to type ids for query optimization + * + * @var array + */ + protected $types = array( + 'soft_state' => 0, + 'hard_state' => 1 + ); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ), + 'statehistory' => array( + 'id' => 'hh.statehistory_id', + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'object_id' => 'hh.object_id', + 'object_type' => '(\'host\')', + 'output' => '(CASE WHEN hh.state_type = 1 THEN hh.output ELSE \'[ \' || hh.current_check_attempt || \'/\' || hh.max_check_attempts || \' ] \' || hh.output END)', + 'state' => 'hh.state', + 'timestamp' => 'UNIX_TIMESTAMP(hh.state_time)', + 'type' => "(CASE WHEN hh.state_type = 1 THEN 'hard_state' ELSE 'soft_state' END)" + ), + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression) { + switch ($filter->getColumn()) { + case 'timestamp': + $this->requireColumn('timestamp'); + $filter->setColumn('hh.state_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + case 'type': + if (! is_array($filter->getExpression())) { + $this->requireColumn('type'); + $filter->setColumn('hh.state_type'); + if (isset($this->types[$filter->getExpression()])) { + $filter->setExpression($this->types[$filter->getExpression()]); + } else { + $filter->setExpression(-1); + } + + return null; + } + } + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('hh' => $this->prefix . 'statehistory'), + array() + )->join( + array('ho' => $this->prefix . 'objects'), + 'ho.object_id = hh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + $this->joinedVirtualTables['statehistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = hh.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = ho.object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php new file mode 100644 index 0000000..e1b5480 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php @@ -0,0 +1,338 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class HoststatusQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('hosts' => array('ho.object_id', 'h.host_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'checktimeperiods' => array( + 'host_check_timeperiod' => 'ctp.alias COLLATE latin1_general_ci' + ), + 'contacts' => [ + 'host_contact' => 'hco.name1' + ], + 'contactgroups' => [ + 'host_contactgroup' => 'hcgo.name1' + ], + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_action_url' => 'h.action_url', + 'host_address' => 'h.address', + 'host_address6' => 'h.address6', + 'host_alias' => 'h.alias', + 'host_check_interval' => '(h.check_interval * 60)', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci', + 'host_icon_image' => 'h.icon_image', + 'host_icon_image_alt' => 'h.icon_image_alt', + 'host_ipv4' => 'INET_ATON(h.address)', + 'host_name' => 'ho.name1', + 'host_notes' => 'h.notes', + 'host_notes_url' => 'h.notes_url', + 'object_type' => '(\'host\')', + 'object_id' => 'ho.object_id' + ), + 'hoststatus' => array( + 'host_acknowledged' => 'hs.problem_has_been_acknowledged', + 'host_acknowledgement_type' => 'hs.acknowledgement_type', + 'host_active_checks_enabled' => 'hs.active_checks_enabled', + 'host_active_checks_enabled_changed' => 'CASE WHEN hs.active_checks_enabled = h.active_checks_enabled THEN 0 ELSE 1 END', + 'host_attempt' => 'hs.current_check_attempt || \'/\' || hs.max_check_attempts', + 'host_check_command' => 'hs.check_command', + 'host_check_execution_time' => 'hs.execution_time', + 'host_check_latency' => 'hs.latency', + 'host_check_source' => 'hs.check_source', + 'host_check_type' => 'hs.check_type', + 'host_current_check_attempt' => 'hs.current_check_attempt', + 'host_current_notification_number' => 'hs.current_notification_number', + 'host_event_handler' => 'hs.event_handler', + 'host_event_handler_enabled' => 'hs.event_handler_enabled', + 'host_event_handler_enabled_changed' => 'CASE WHEN hs.event_handler_enabled = h.event_handler_enabled THEN 0 ELSE 1 END', + 'host_failure_prediction_enabled' => 'hs.failure_prediction_enabled', + 'host_flap_detection_enabled' => 'hs.flap_detection_enabled', + 'host_flap_detection_enabled_changed' => 'CASE WHEN hs.flap_detection_enabled = h.flap_detection_enabled THEN 0 ELSE 1 END', + 'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END', + 'host_hard_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE CASE WHEN hs.state_type = 1 THEN hs.current_state ELSE hs.last_hard_state END END', + 'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END', + 'host_is_flapping' => 'hs.is_flapping', + 'host_is_passive_checked' => 'CASE WHEN hs.active_checks_enabled = 0 AND hs.passive_checks_enabled = 1 THEN 1 ELSE 0 END', + 'host_is_reachable' => 'hs.is_reachable', + 'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)', + 'host_last_hard_state' => 'hs.last_hard_state', + 'host_last_hard_state_change' => 'UNIX_TIMESTAMP(hs.last_hard_state_change)', + 'host_last_notification' => 'UNIX_TIMESTAMP(hs.last_notification)', + 'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)', + 'host_last_state_change_ts' => 'hs.last_state_change', + 'host_last_time_down' => 'UNIX_TIMESTAMP(hs.last_time_down)', + 'host_last_time_unreachable' => 'UNIX_TIMESTAMP(hs.last_time_unreachable)', + 'host_last_time_up' => 'UNIX_TIMESTAMP(hs.last_time_up)', + 'host_long_output' => 'hs.long_output', + 'host_max_check_attempts' => 'hs.max_check_attempts', + 'host_modified_host_attributes' => 'hs.modified_host_attributes', + 'host_next_check' => 'CASE hs.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END', + 'host_next_notification' => 'UNIX_TIMESTAMP(hs.next_notification)', + 'host_next_update' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL + THEN + CASE hs.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(hs.next_check) + (hs.normal_check_interval * 60) ELSE NULL END + ELSE + UNIX_TIMESTAMP(hs.next_check) + + (CASE WHEN + COALESCE(hs.current_state, 0) > 0 AND hs.state_type = 0 + THEN + hs.retry_check_interval + ELSE + hs.normal_check_interval + END * 60) + + (CEIL(hs.execution_time + hs.latency) * 2) + END', + 'host_no_more_notifications' => 'hs.no_more_notifications', + 'host_normal_check_interval' => 'hs.normal_check_interval', + 'host_notifications_enabled' => 'hs.notifications_enabled', + 'host_notifications_enabled_changed' => 'CASE WHEN hs.notifications_enabled = h.notifications_enabled THEN 0 ELSE 1 END', + 'host_obsessing' => 'hs.obsess_over_host', + 'host_obsessing_changed' => 'CASE WHEN hs.obsess_over_host = h.obsess_over_host THEN 0 ELSE 1 END', + 'host_output' => 'hs.output', + 'host_passive_checks_enabled' => 'hs.passive_checks_enabled', + 'host_passive_checks_enabled_changed' => 'CASE WHEN hs.passive_checks_enabled = h.passive_checks_enabled THEN 0 ELSE 1 END', + 'host_percent_state_change' => 'hs.percent_state_change', + 'host_perfdata' => 'hs.perfdata', + 'host_problem' => 'CASE WHEN COALESCE(hs.current_state, 0) = 0 THEN 0 ELSE 1 END', + 'host_problem_has_been_acknowledged' => 'hs.problem_has_been_acknowledged', + 'host_process_performance_data' => 'hs.process_performance_data', + 'host_retry_check_interval' => 'hs.retry_check_interval', + 'host_scheduled_downtime_depth' => 'hs.scheduled_downtime_depth', + 'host_severity' => ' + CASE + WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL + THEN 16 + ELSE + CASE + WHEN hs.current_state = 0 + THEN 1 + ELSE + CASE + WHEN hs.current_state = 1 THEN 64 + WHEN hs.current_state = 2 THEN 32 + ELSE 256 + END + + + CASE + WHEN hs.problem_has_been_acknowledged = 1 THEN 2 + WHEN hs.scheduled_downtime_depth > 0 THEN 1 + ELSE 256 + END + END + END', + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END', + 'host_state_type' => 'hs.state_type', + 'host_status_update_time' => 'hs.status_update_time', + 'host_unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END', + 'problems' => 'CASE WHEN COALESCE(hs.current_state, 0) = 0 THEN 0 ELSE 1 END' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.10.0', '<')) { + $this->columnMap['hoststatus']['host_check_source'] = '(NULL)'; + } + if (version_compare($this->getIdoVersion(), '1.13.0', '<')) { + $this->columnMap['hoststatus']['host_is_reachable'] = '(NULL)'; + } + + $this->select->from( + array('ho' => $this->prefix . 'objects'), + array() + )->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = ho.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + ); + $this->joinedVirtualTables['hosts'] = true; + } + + /** + * Join check time periods + */ + protected function joinChecktimeperiods() + { + $this->select->joinLeft( + array('ctp' => $this->prefix . 'timeperiods'), + 'ctp.timeperiod_object_id = h.check_timeperiod_object_id', + array() + ); + } + + /** + * Join contacts + */ + protected function joinContacts() + { + $this->select->joinLeft( + ['hc' => 'icinga_host_contacts'], + 'hc.host_id = h.host_id', + [] + )->joinLeft( + ['hco' => 'icinga_objects'], + 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10', + [] + ); + } + + /** + * Join contact groups + */ + protected function joinContactgroups() + { + $this->select->joinLeft( + ['hcg' => 'icinga_host_contactgroups'], + 'hcg.host_id = h.host_id', + [] + )->joinLeft( + ['hcgo' => 'icinga_objects'], + 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = ho.object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = ho.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = ho.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = s.service_object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sg.' . $this->servicegroup_id, + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->requireVirtualTable('hosts'); + $this->select->joinLeft( + array('s' => $this->prefix . 'services'), + 's.host_object_id = h.host_object_id', + array() + )->joinLeft( + array('so' => $this->prefix . 'objects'), + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 'ho.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('services'); + + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php new file mode 100644 index 0000000..5d79143 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php @@ -0,0 +1,91 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host group summaries + */ +class HoststatussummaryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'hoststatussummary' => array( + 'hosts_down' => 'SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END)', + 'hosts_down_handled' => 'SUM(CASE WHEN state = 1 AND handled = 1 THEN 1 ELSE 0 END)', + 'hosts_down_unhandled' => 'SUM(CASE WHEN state = 1 AND handled = 0 THEN 1 ELSE 0 END)', + 'hosts_pending' => 'SUM(CASE WHEN state = 99 THEN 1 ELSE 0 END)', + 'hosts_total' => 'SUM(1)', + 'hosts_unreachable' => 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END)', + 'hosts_unreachable_handled' => 'SUM(CASE WHEN state = 2 AND handled = 1 THEN 1 ELSE 0 END)', + 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN state = 2 AND handled = 0 THEN 1 ELSE 0 END)', + 'hosts_up' => 'SUM(CASE WHEN state = 0 THEN 1 ELSE 0 END)' + ) + ); + + /** + * The host status sub select + * + * @var HostStatusQuery + */ + protected $subSelect; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + return $this->subSelect->allowsCustomVars(); + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + $this->subSelect->applyFilter(clone $filter); + return $this; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + // TODO(el): Allow to switch between hard and soft states + $this->subSelect = $this->createSubQuery( + 'Hoststatus', + array( + 'handled' => 'host_handled', + 'state' => 'host_state', + 'state_change' => 'host_last_state_change' + ) + ); + $this->select->from( + array('hoststatussummary' => $this->subSelect->setIsSubQuery(true)), + array() + ); + $this->joinedVirtualTables['hoststatussummary'] = true; + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->subSelect->where($condition, $value); + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->subSelect->whereEx($ex); + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php new file mode 100644 index 0000000..bd7a077 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php @@ -0,0 +1,1599 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterNot; +use Zend_Db_Expr; +use Icinga\Application\Icinga; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Data\Db\DbQuery; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotImplementedError; +use Icinga\Exception\ProgrammingError; +use Icinga\Exception\QueryException; +use Icinga\Web\Session; +use Icinga\Module\Monitoring\Data\ColumnFilterIterator; +use Zend_Db_Select; + +/** + * Base class for Ido Queries + * + * This is the base class for all Ido queries and should be extended for new queries + * The starting point for implementations is the columnMap attribute. This is an asscociative array in the + * following form: + * + * <pre> + * <code> + * array( + * 'virtualTable' => array( + * 'fieldalias1' => 'queryColumn1', + * 'fieldalias2' => 'queryColumn2', + * .... + * ), + * 'virtualTable2' => array( + * 'host' => 'host_name1' + * ) + * ) + * </code> + * </pre> + * + * This allows you to select e.g. fieldalias1, which automatically calls the query code for joining 'virtualTable'. If + * you afterwards select 'host', 'virtualTable2' will be joined. The joining logic is up to you, in order to make the + * above example work you need to implement the joinVirtualTable() method which contain your + * custom (Zend_Db) logic for joining, filtering and querying the data you want. + * + */ +abstract class IdoQuery extends DbQuery +{ + /** + * The prefix to use + * + * @var string + */ + protected $prefix; + + /** + * An array to map aliases to column names + * + * @var array + */ + protected $idxAliasColumn; + + /** + * An array to map aliases to table names + * + * @var array + */ + protected $idxAliasTable; + + /** + * An array to map custom aliases to aliases + * + * @var array + */ + protected $idxCustomAliases; + + /** + * The column map containing all filterable columns + * + * This must be overwritten by child classes, in the format + * array( + * 'virtualTable' => array( + * 'fieldalias1' => 'queryColumn1', + * 'fieldalias2' => 'queryColumn2', + * .... + * ) + * ) + * + * @var array + */ + protected $columnMap = array(); + + /** + * Custom vars available for this query + * + * @var array + */ + protected $customVars = array(); + + /** + * Printf compatible string to joins custom vars + * + * - %1$s Source field, contain the object_id + * - %2$s Alias used for the relation + * - %3$s Name of the CustomVariable + * + * @var string + */ + private $customVarsJoinTemplate = '%1$s = %2$s.object_id AND %2$s.varname = %3$s'; + + /** + * An array with all 'virtual' tables that are already joined + * + * Virtual tables are the keys of the columnMap array and require a + * join%VirtualTableName%() method to be defined in the concrete + * query + * + * @var array + */ + protected $joinedVirtualTables = array(); + + /** + * A map of virtual table names and corresponding hook instances + * + * Joins for those tables will be delegated to them + * + * @var array + */ + protected $hookedVirtualTables = array(); + + /** + * List of column aliases used for sorting the result + * + * @var array + */ + protected $orderColumns = array(); + + /** + * Table to columns map which have to be added to the GROUP BY list if the query is grouped + * + * @var array + */ + protected $groupBase = array(); + + /** + * List of table names which initiate grouping if one of them is joined + * + * @var array + */ + protected $groupOrigin = array(); + + /** + * Map of table names to query names for which to create subquery filters + * + * @var array + */ + protected $subQueryTargets = array(); + + /** + * The primary key column for the instances table + * + * @var string + */ + protected $instance_id = 'instance_id'; + + /** + * The primary key column for the objects table + * + * @var string + */ + protected $object_id = 'object_id'; + + /** + * The primary key column for the acknowledgements table + * + * @var string + */ + protected $acknowledgement_id = 'acknowledgement_id'; + + /** + * The primary key column for the commenthistory table + * + * @var string + */ + protected $commenthistory_id = 'commenthistory_id'; + + /** + * The primary key column for the contactnotifications table + * + * @var string + */ + protected $contactnotification_id = 'contactnotification_id'; + + /** + * The primary key column for the downtimehistory table + * + * @var string + */ + protected $downtimehistory_id = 'downtimehistory_id'; + + /** + * The primary key column for the flappinghistory table + * + * @var string + */ + protected $flappinghistory_id = 'flappinghistory_id'; + + /** + * The primary key column for the notifications table + * + * @var string + */ + protected $notification_id = 'notification_id'; + + /** + * The primary key column for the statehistory table + * + * @var string + */ + protected $statehistory_id = 'statehistory_id'; + + /** + * The primary key column for the comments table + * + * @var string + */ + protected $comment_id = 'comment_id'; + + /** + * The primary key column for the customvariablestatus table + * + * @var string + */ + protected $customvariablestatus_id = 'customvariablestatus_id'; + + /** + * The primary key column for the hoststatus table + * + * @var string + */ + protected $hoststatus_id = 'hoststatus_id'; + + /** + * The primary key column for the programstatus table + * + * @var string + */ + protected $programstatus_id = 'programstatus_id'; + + /** + * The primary key column for the runtimevariables table + * + * @var string + */ + protected $runtimevariable_id = 'runtimevariable_id'; + + /** + * The primary key column for the scheduleddowntime table + * + * @var string + */ + protected $scheduleddowntime_id = 'scheduleddowntime_id'; + + /** + * The primary key column for the servicestatus table + * + * @var string + */ + protected $servicestatus_id = 'servicestatus_id'; + + /** + * The primary key column for the contactstatus table + * + * @var string + */ + protected $contactstatus_id = 'contactstatus_id'; + + /** + * The primary key column for the commands table + * + * @var string + */ + protected $command_id = 'command_id'; + + /** + * The primary key column for the contactgroup_members table + * + * @var string + */ + protected $contactgroup_member_id = 'contactgroup_member_id'; + + /** + * The primary key column for the contactgroups table + * + * @var string + */ + protected $contactgroup_id = 'contactgroup_id'; + + /** + * The primary key column for the contacts table + * + * @var string + */ + protected $contact_id = 'contact_id'; + + /** + * The primary key column for the customvariables table + * + * @var string + */ + protected $customvariable_id = 'customvariable_id'; + + /** + * The primary key column for the host_contactgroups table + * + * @var string + */ + protected $host_contactgroup_id = 'host_contactgroup_id'; + + /** + * The primary key column for the host_contacts table + * + * @var string + */ + protected $host_contact_id = 'host_contact_id'; + + /** + * The primary key column for the hostgroup_members table + * + * @var string + */ + protected $hostgroup_member_id = 'hostgroup_member_id'; + + /** + * The primary key column for the hostgroups table + * + * @var string + */ + protected $hostgroup_id = 'hostgroup_id'; + + /** + * The primary key column for the hosts table + * + * @var string + */ + protected $host_id = 'host_id'; + + /** + * The primary key column for the service_contactgroup table + * + * @var string + */ + protected $service_contactgroup_id = 'service_contactgroup_id'; + + /** + * The primary key column for the service_contact table + * + * @var string + */ + protected $service_contact_id = 'service_contact_id'; + + /** + * The primary key column for the servicegroup_members table + * + * @var string + */ + protected $servicegroup_member_id = 'servicegroup_member_id'; + + /** + * The primary key column for the servicegroups table + * + * @var string + */ + protected $servicegroup_id = 'servicegroup_id'; + + /** + * The primary key column for the services table + * + * @var string + */ + protected $service_id = 'service_id'; + + /** + * The primary key column for the timeperiods table + * + * @var string + */ + protected $timeperiod_id = 'timeperiod_id'; + + /** + * An array containing Column names that cause an aggregation of the query + * + * @var array + */ + protected $aggregateColumnIdx = array(); + + /** + * True to allow customvar filters and queries + * + * @var bool + */ + protected $allowCustomVars = false; + + /** + * Current IDO version. This is bullshit and needs to be moved somewhere + * else. As someone decided that we need no Backend-specific connection + * class unfortunately there is no better place right now. And as of the + * 'check_source' patch we need a quick fix immediately. So here you go. + * + * TODO: Fix this. + * + * @var string + */ + protected static $idoVersion; + + /** + * List of column aliases mapped to their table where the COLLATE SQL-instruction has been removed + * + * This list is being populated in case of a PostgreSQL backend only, + * to ensure case-insensitive string comparison in WHERE clauses. + * + * @var array + */ + protected $caseInsensitiveColumns; + + /** + * Return true when the column is an aggregate column + * + * @param String $column The column to test + * @return bool True when the column is an aggregate column + */ + public function isAggregateColumn($column) + { + return array_key_exists($column, $this->aggregateColumnIdx); + } + + /** + * Order the result by the given alias + * + * @param string $alias The column alias to order by + * @param int $dir The sort direction or null to use the default direction + * + * @return $this + */ + public function order($alias, $dir = null) + { + $this->requireColumn($alias); + + if ($this->isCustomvar($alias)) { + $column = $this->getCustomvarColumnName($alias); + } elseif ($this->hasAliasName($alias)) { + $column = $this->aliasToColumnName($alias); + $table = $this->aliasToTableName($alias); + if (isset($this->caseInsensitiveColumns[$table][$alias])) { + $column = 'LOWER(' . $column . ')'; + } + } else { + Logger::info('Can\'t order by column ' . $alias); + return $this; + } + + $this->orderColumns[] = $alias; + return parent::order($column, $dir); + } + + /** + * Return true when the given field can be used for filtering + * + * @param String $field The field to test + * @return bool True when the field can be used for querying, otherwise false + */ + public function isValidFilterTarget($field) + { + return $this->getMappedField($field) !== null; + } + + /** + * Return the resolved field for an alias + * + * @param String $field The alias to resolve + * @return String The resolved alias or null if unknown + */ + public function getMappedField($field) + { + foreach ($this->columnMap as $columnSource => $columnSet) { + if (isset($columnSet[$field])) { + return $columnSet[$field]; + } + } + if ($this->isCustomVar($field)) { + return $this->getCustomvarColumnName($field); + } + return null; + } + + public function distinct() + { + $this->select->distinct(); + return $this; + } + + /** + * Prepare the given query so that it can be linked to the parent + * + * @param IdoQuery $query + * @param string $name + * @param FilterExpression $filter The filter which initiated the sub query + * @param bool $and Whether it's an AND filter + * @param bool $negate Whether it's an != filter + * @param FilterExpression $additionalFilter Filters which should be applied to the "parent" query + * + * @return array The first value is their, the second our key column + * + * @throws NotImplementedError In case the given query is unknown + */ + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + throw new NotImplementedError('Query "%s" is unknown', $name); + } + + /** + * Create and return a sub-query filter for the given filter expression + * + * @param FilterExpression $filter + * @param string $queryName + * + * @return Filter + * + * @throws QueryException + */ + protected function createSubQueryFilter(FilterExpression $filter, $queryName) + { + $expr = $filter->getExpression(); + $op = $filter->getSign(); + + if ($op === '=' && ! is_array($expr) && $op !== '!=') { + // We're joining a subquery only if the filter is enclosed in parentheses or if it's a != filter, + // e.g. hostgroup_name=(linux...), hostgroup_name!=linux, hostgroup_name!=(linux...) + throw new NotImplementedError(''); + } + + $subQuery = $this->createSubQuery($queryName); + $subQuery->setIsSubQuery(); + + $subQueryFilter = clone $filter; + + if ($op === '!=') { + $negate = true; + if (! is_array($expr)) { + // We assume that expression is an array later on but we'll support subquery joins for != filters + // which are not enclosed in parentheses + $expr = [$expr]; + } + } else { + $negate = false; + } + + if (count($expr) === 1 && strpos($expr[0], '&') !== false) { + // Our current filter implementation does not specify & as a control character so the count of the + // expression array is always one in this case + $expr = array_unique(explode('&', $expr[0])); + $subQueryFilter->setExpression($expr); + $and = true; + } else { + // Or filters are respected by our filter implementation. No special handling needed here + $and = false; + } + + $alias = $filter->getColumn(); + $column = $subQuery->aliasToColumnName($alias); + if (isset($this->caseInsensitiveColumns[$subQuery->aliasToTableName($alias)][$alias])) { + $column = 'LOWER( ' . $column . ' )'; + $subQueryFilter->setColumn($column); + $subQueryFilter->setExpression(array_map('strtolower', (array) $subQueryFilter->getExpression())); + } else { + $subQueryFilter->setColumn($column); + } + + $additional = null; + + list($theirs, $ours) = $this->joinSubQuery($subQuery, $queryName, $subQueryFilter, $and, $negate, $additional); + + $zendSelect = $subQuery->select(); + $fromPart = $zendSelect->getPart($zendSelect::FROM); + $zendSelect->reset($zendSelect::FROM); + + foreach ($fromPart as $correlationName => $joinOptions) { + if (isset($joinOptions['joinCondition'])) { + $joinOptions['joinCondition'] = preg_replace( + '/(?<=^|\s)\w+(?=\.)/', + 'sub_$0', + $joinOptions['joinCondition'] + ); + } + + $name = ['sub_' . $correlationName => $joinOptions['tableName']]; + switch ($joinOptions['joinType']) { + case $zendSelect::FROM: + $zendSelect->from($name); + break; + case $zendSelect::INNER_JOIN: + $zendSelect->joinInner($name, $joinOptions['joinCondition'], null); + break; + case $zendSelect::LEFT_JOIN: + $zendSelect->joinLeft($name, $joinOptions['joinCondition'], null); + break; + default: + // TODO: Add support for other join types if required? + throw new QueryException( + 'Unsupported join type %s. Cannot create subquery filter.', + $joinOptions['joinType'] + ); + } + } + + if ($and || $negate) { + // Having is only required for AND and != filters, + // e.g. hostgroup_name=(ping&linux), hostgroup_name!=ping, hostgroup_name!=(ping|linux) + $groups = $subQuery->getGroup(); + if (! empty($groups)) { + $group = $groups[0]; + $group = preg_replace('/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $group); + + $cnt = count($expr); + + $subQuery->select()->having("COUNT(DISTINCT $group) >= $cnt"); + } + } + + $subQueryFilter->setColumn(preg_replace( + '/(?<=^|\s)\w+(?=\.)/', + 'sub_$0', + $column + )); + + if ($negate) { + // != will be NOT EXISTS later + $subQueryFilter = $subQueryFilter->setSign('='); + } + + $subQueryFilter = $subQueryFilter->andFilter(Filter::where( + preg_replace('/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $theirs), + new Zend_Db_Expr($ours) + )); + + $subQuery + ->setFilter($subQueryFilter) + ->clearGroupingRules() + ->select() + ->reset('columns') + ->columns([new Zend_Db_Expr('1')]); + + // EXISTS is the column name because without any column $this->isCustomVar() fails badly otherwise. + // Additionally it bypasses the non-required optimizations made by our filter rendering implementation. + $exists = new FilterExpression($negate ? 'NOT EXISTS' : 'EXISTS', '', new Zend_Db_Expr($subQuery)); + + if ($additional !== null) { + return Filter::matchAll($exists, $additional); + } + + return $exists; + } + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression) { + $alias = $filter->getColumn(); + + $virtualTable = $this->aliasToTableName($alias); + if (isset($this->subQueryTargets[$virtualTable])) { + try { + return $this->createSubQueryFilter($filter, $this->subQueryTargets[$virtualTable]); + } catch (NotImplementedError $e) { + // We don't want to create subquery filters in all cases + } + } + + $this->requireColumn($alias); + + if ($this->isCustomvar($alias)) { + $column = $this->getCustomvarColumnName($alias); + } else { + $column = $this->aliasToColumnName($alias); + if (isset($this->caseInsensitiveColumns[$this->aliasToTableName($alias)][$alias])) { + $column = 'LOWER(' . $column . ')'; + $expression = $filter->getExpression(); + if (is_array($expression)) { + $filter->setExpression(array_map('strtolower', $expression)); + } else { + $filter->setExpression(strtolower($expression)); + } + } + } + + $filter->setColumn($column); + } else { + if (! $filter instanceof FilterNot) { + // Allow subquery filters in a filter chain + $columns = $filter->listFilteredColumns(); + if (count($columns) === 1) { + $column = $columns[0]; + $virtualTable = $this->aliasToTableName($column); + if (isset($this->subQueryTargets[$virtualTable])) { + $lastSign = null; + $filters = []; + $expressions = []; + foreach ($filter->filters() as $child) { + switch (true) { + case $child instanceof FilterExpression: + $expression = $child->getExpression(); + if (! is_array($expression)) { + break; + } + // Move to default + default: + $filters[] = $child; + continue 2; + } + if ($lastSign === null) { + $lastSign = $child->getSign(); + } else { + $sign = $child->getSign(); + if ($sign !== $lastSign) { + $filters[] = new FilterExpression( + $column, + $lastSign, + $filter->getOperatorSymbol() === '&' + ? [implode('&', $expressions)] + : $expressions + ); + $expressions = []; + $lastSign = $sign; + } + } + $expressions[] = $expression; + } + if (! empty($expressions)) { + $filters[] = new FilterExpression( + $column, + $lastSign, + $filter->getOperatorSymbol() === '&' + ? [implode('&', $expressions)] + : $expressions + ); + } + $filter->setFilters($filters); + } + } + } + + foreach ($filter->filters() as $child) { + $replacement = $this->requireFilterColumns($child); + if ($replacement !== null) { + // setId($child->getId()) is performed because replaceById() doesn't already do it + $filter->replaceById($child->getId(), $replacement->setId($child->getId())); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + $filter = clone $filter; + return parent::addFilter($this->requireFilterColumns($filter) ?: $filter); + } + + public function where($condition, $value = null) + { + $this->requireColumn($condition); + $col = $this->getMappedField($condition); + if ($col === null) { + throw new IcingaException( + 'No such field: %s', + $condition + ); + } + return parent::where($col, $value); + } + + /** + * Add a filter expression, with as less validation as possible + * + * @param FilterExpression $ex + * + * @internal If you use this outside the monitoring module, it's your fault if something breaks + * @return $this + */ + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + $col = $this->getMappedField($ex->getColumn()); + if ($col === null) { + throw new IcingaException( + 'No such field: %s', + $ex->getColumn() + ); + } + + parent::addFilter((clone $ex)->setColumn($col)); + + return $this; + } + + /** + * Return true if an field contains an explicit timestamp + * + * @param string $field The field to test for containing an timestamp + * + * @return bool True when the field represents an timestamp + */ + public function isTimestamp($field) + { + if ($this->isCustomVar($field)) { + return false; + } + + return stripos($this->getMappedField($field) ?: $field, 'UNIX_TIMESTAMP') !== false; + } + + /** + * Return whether the given alias provides case insensitive value comparison + * + * @param string $alias + * + * @return bool + */ + public function isCaseInsensitive($alias) + { + if ($this->isCustomVar($alias)) { + return false; + } + + $column = $this->getMappedField($alias); + if (! $column) { + return false; + } + + if (empty($this->caseInsensitiveColumns)) { + return preg_match('/ COLLATE .+$/', $column) === 1; + } + + if (strpos($column, 'LOWER') === 0) { + return true; + } + + $table = $this->aliasToTableName($alias); + if (! $table) { + return false; + } + + return isset($this->caseInsensitiveColumns[$table][$alias]); + } + + /** + * Return our column map + * + * Might be useful for hooks + * + * @return array + */ + public function getColumnMap() + { + return $this->columnMap; + } + + /** + * Apply oracle specific query initialization + */ + private function initializeForOracle() + { + // Oracle uses the reserved field 'id' for primary keys, so + // these must be used instead of the normally defined ids + $this->object_id = $this->host_id = $this->service_id + = $this->hostgroup_id = $this->servicegroup_id + = $this->contact_id = $this->contactgroup_id = 'id'; + $this->customVarsJoinTemplate = + '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s'; + foreach ($this->columnMap as &$columns) { + foreach ($columns as &$value) { + $value = preg_replace('/UNIX_TIMESTAMP/', 'localts2unixts', $value); + $value = preg_replace('/ COLLATE .+$/', '', $value); + } + } + } + + /** + * Apply PostgreSQL specific query initialization + */ + private function initializeForPostgres() + { + $this->customVarsJoinTemplate = + '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s'; + foreach ($this->columnMap as $table => & $columns) { + foreach ($columns as $alias => & $column) { + if ($column === null) { + continue; + } + + // Using a regex here because COLLATE may occur anywhere in the string + $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count); + if ($count > 0) { + $this->caseInsensitiveColumns[$table][$alias] = true; + } + + $column = preg_replace( + '/inet_aton\(([[:word:].]+)\)/i', + '(CASE WHEN $1 ~ \'(?:[0-9]{1,3}\\\\.){3}[0-9]{1,3}\' THEN $1::inet - \'0.0.0.0\' ELSE NULL END)', + $column + ); + if (version_compare($this->getIdoVersion(), '1.14.2', '>=')) { + $column = str_replace('NOW()', 'NOW() AT TIME ZONE \'UTC\'', $column); + } else { + $column = preg_replace( + '/UNIX_TIMESTAMP(\((?>[^()]|(?-1))*\))/i', + 'CASE WHEN ($1 < \'1970-01-03 00:00:00+00\'::timestamp with time zone) THEN 0 ELSE UNIX_TIMESTAMP($1) END', + $column + ); + } + } + } + } + + /** + * Set up this query and join the initial tables + * + * @see IdoQuery::initializeForPostgres For postgresql specific setup + */ + protected function init() + { + parent::init(); + $this->prefix = $this->ds->getTablePrefix(); + + foreach (Hook::all('monitoring/idoQueryExtension') as $hook) { + $extensions = $hook->extendColumnMap($this); + if (! is_array($extensions)) { + continue; + } + + foreach ($extensions as $vTable => $cols) { + if (! array_key_exists($vTable, $this->columnMap)) { + $this->hookedVirtualTables[$vTable] = $hook; + $this->columMap[$vTable] = array(); + } + + foreach ($cols as $k => $v) { + $this->columnMap[$vTable][$k] = $v; + } + } + } + + $dbType = $this->ds->getDbType(); + if ($dbType === 'oracle') { + $this->initializeForOracle(); + } elseif ($dbType === 'pgsql') { + $this->initializeForPostgres(); + } else { + $charset = $this->ds->getConfig()->get('charset') ?: 'latin1'; + $this->customVarsJoinTemplate .= " COLLATE {$charset}_general_ci"; + } + $this->joinBaseTables(); + $this->select->columns($this->columns); + $this->prepareAliasIndexes(); + } + + /** + * Join the base tables for this query + */ + protected function joinBaseTables() + { + reset($this->columnMap); + $table = key($this->columnMap); + + $this->select->from( + array($table => $this->prefix . $table), + array() + ); + + $this->joinedVirtualTables = array($table => true); + } + + /** + * Populates the idxAliasTAble and idxAliasColumn properties + */ + protected function prepareAliasIndexes() + { + foreach ($this->columnMap as $tbl => & $cols) { + foreach ($cols as $alias => $col) { + $this->idxAliasTable[$alias] = $tbl; + $this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col); + } + } + } + + /** + * Resolve columns aliases to their database field using the columnMap + * + * @param array $columns + * + * @return array + */ + public function resolveColumns($columns) + { + $resolvedColumns = array(); + + foreach ($columns as $alias => $col) { + if ($col instanceof Zend_Db_Expr) { + // Support selecting NULL as column for example + $resolvedColumns[$alias] = $col; + continue; + } + $this->requireColumn($col); + if ($this->isCustomvar($col)) { + $name = $this->getCustomvarColumnName($col); + } else { + $name = $this->aliasToColumnName($col); + } + if (is_int($alias)) { + $alias = $col; + } else { + $this->idxCustomAliases[$alias] = $col; + } + + $resolvedColumns[$alias] = preg_replace('|\n|', ' ', $name); + } + + return $resolvedColumns; + } + + /** + * Return all columns that will be selected when no columns are given in the constructor or from + * + * @return array An array of column aliases + */ + public function getDefaultColumns() + { + reset($this->columnMap); + $table = key($this->columnMap); + return array_keys($this->columnMap[$table]); + } + + /** + * Modify the query to the given alias can be used in the result set or queries + * + * This calls requireVirtualTable if needed + * + * @param string $alias The alias of the column to require + * + * @return $this Fluent interface + * @see IdoQuery::requireVirtualTable The method initializing required joins + * @throws \Icinga\Exception\ProgrammingError When an unknown column is requested + */ + public function requireColumn($alias) + { + if ($this->hasAliasName($alias)) { + $this->requireVirtualTable($this->aliasToTableName($alias)); + } elseif ($this->isCustomVar($alias)) { + $this->requireCustomvar($alias); + } else { + throw new ProgrammingError( + '%s : Got invalid column: %s', + get_called_class(), + $alias + ); + } + return $this; + } + + /** + * Return true if the given alias exists + * + * @param String $alias The alias to test for + * @return bool True when the alias exists, otherwise false + */ + protected function hasAliasName($alias) + { + return array_key_exists($alias, $this->idxAliasColumn); + } + + /** + * Require a virtual table for the given table name if not already required + * + * @param String $name The table name to require + * @return $this Fluent interface + */ + protected function requireVirtualTable($name) + { + if ($this->hasJoinedVirtualTable($name)) { + return $this; + } + + if ($this->virtualTableIsHooked($name)) { + return $this->joinHookedVirtualTable($name); + } else { + return $this->joinVirtualTable($name); + } + } + + /** + * Whether a given virtual table name has been provided by a hook + * + * @param string $name Virtual table name + * + * @return boolean + */ + protected function virtualTableIsHooked($name) + { + return array_key_exists($name, $this->hookedVirtualTables); + } + + protected function conflictsWithVirtualTable($name) + { + if ($this->hasJoinedVirtualTable($name)) { + throw new ProgrammingError( + 'IDO query virtual table conflict with "%s"', + $name + ); + } + return $this; + } + + /** + * Call the method for joining a virtual table + * + * This requires a join$Table() method to exist + * + * @param String $table The table to join by calling join$Table() in the concrete implementation + * @return $this Fluent interface + * + * @throws \Icinga\Exception\ProgrammingError If the join method for this table does not exist + */ + protected function joinVirtualTable($table) + { + $func = 'join' . ucfirst($table); + if (method_exists($this, $func)) { + $this->$func(); + } else { + throw new ProgrammingError( + 'Cannot join "%s", no such table found', + $table + ); + } + $this->joinedVirtualTables[$table] = true; + return $this; + } + + /** + * Tell a hook to join a virtual table + * + * @param String $table + * @return $this + */ + protected function joinHookedVirtualTable($table) + { + $this->hookedVirtualTables[$table]->joinVirtualTable($this, $table); + $this->joinedVirtualTables[$table] = true; + return $this; + } + + /** + * Get the table for a specific alias + * + * @param String $alias The alias to request the table for + * @return String The table for the alias or null if it doesn't exist + */ + protected function aliasToTableName($alias) + { + return isset($this->idxAliasTable[$alias]) ? $this->idxAliasTable[$alias] : null; + } + + /** + * Return whether this query allows to join custom variables + * + * @return bool + */ + public function allowsCustomVars() + { + return $this->allowCustomVars; + } + + /** + * Return true if the given alias denotes a custom variable + * + * @param String $alias The alias to test for being a customvariable + * @return bool True if the alias is a customvariable, otherwise false + */ + protected function isCustomVar($alias) + { + return $this->allowCustomVars && $alias[0] === '_'; + } + + protected function requireCustomvar($customvar) + { + if (! $this->hasCustomvar($customvar)) { + $this->joinCustomvar($customvar); + } + return $this; + } + + protected function hasCustomvar($customvar) + { + return array_key_exists(strtolower($customvar), $this->customVars); + } + + protected function joinCustomvar($customvar) + { + // TODO: This is not generic enough yet + list($type, $name) = $this->customvarNameToTypeName($customvar); + $alias = ($type === 'host' ? 'hcv_' : 'scv_') . preg_replace('~[^a-zA-Z0-9_]~', '_', $name); + + // We're replacing any problematic char with an underscore, which will lead to duplicates, this avoids them + $from = $this->select->getPart(Zend_Db_Select::FROM); + for ($i = 2; array_key_exists($alias, $from); $i++) { + $alias = $alias . '_' . $i; + } + + $this->customVars[strtolower($customvar)] = $alias; + + if ($type === 'host') { + if ($this instanceof ServicecommentQuery + || $this instanceof ServicedowntimeQuery + || $this instanceof ServicecommenthistoryQuery + || $this instanceof ServicedowntimestarthistoryQuery + || $this instanceof ServiceflappingstarthistoryQuery + || $this instanceof ServicegroupQuery + || $this instanceof ServicenotificationQuery + || $this instanceof ServicestatehistoryQuery + || $this instanceof ServicestatusQuery + ) { + $this->requireVirtualTable('services'); + $leftcol = 's.host_object_id'; + } else { + $leftcol = 'ho.object_id'; + if (! $this->hasJoinedTable('ho')) { + $this->requireVirtualTable('hosts'); + } + } + } else { // $type === 'service' + $leftcol = 'so.object_id'; + if (! $this->hasJoinedTable('so')) { + $this->requireVirtualTable('services'); + } + } + + $mapped = $this->getMappedField($leftcol); + if ($mapped !== null) { + $this->requireColumn($leftcol); + $leftcol = $mapped; + } + + $joinOn = sprintf( + $this->customVarsJoinTemplate, + $leftcol, + $alias, + $this->db->quote($name) + ); + + $this->select->joinLeft( + array($alias => $this->prefix . 'customvariablestatus'), + $joinOn, + array() + ); + + return $this; + } + + protected function customvarNameToTypeName($customvar) + { + $customvar = strtolower($customvar); + if (! preg_match('~^_(host|service)_(.+)$~', $customvar, $m)) { + throw new ProgrammingError( + 'Got invalid custom var: "%s"', + $customvar + ); + } + return array($m[1], $m[2]); + } + + protected function hasJoinedVirtualTable($name) + { + return array_key_exists($name, $this->joinedVirtualTables); + } + + /** + * Get the query column of a already joined custom variable + * + * @param string $customvar + * + * @return string + * @throws QueryException If the custom variable has not been joined + */ + protected function getCustomvarColumnName($customvar) + { + if (! isset($this->customVars[($customvar = strtolower($customvar))])) { + throw new QueryException('Custom variable %s has not been joined', $customvar); + } + return $this->customVars[$customvar] . '.varvalue'; + } + + public function aliasToColumnName($alias) + { + return $this->idxAliasColumn[$alias]; + } + + /** + * Get the alias of a column expression as defined in the {@link $columnMap} property. + * + * @param string $alias Potential custom alias + * + * @return string + */ + public function customAliasToAlias($alias) + { + if (isset($this->idxCustomAliases[$alias])) { + return $this->idxCustomAliases[$alias]; + } + return $alias; + } + + /** + * Create a sub query + * + * @param string $queryName + * @param array $columns + * + * @return static + */ + protected function createSubQuery($queryName, $columns = array()) + { + $class = '\\' + . substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1) + . ucfirst($queryName) . 'Query'; + $query = new $class($this->ds, $columns); + return $query; + } + + /** + * Set columns to select + * + * @param array $columns + * + * @return $this + */ + public function columns(array $columns) + { + $this->idxCustomAliases = array(); + $this->columns = $this->resolveColumns($columns); + // TODO: we need to refresh our select! + // $this->select->columns($columns); + return $this; + } + + public function clearGroupingRules() + { + $this->groupBase = array(); + $this->groupOrigin = array(); + return $this; + } + + /** + * Register the GROUP BY columns required for the given alias + * + * @param string $alias The alias to register columns for + * @param string $table The table the given alias is associated with + * @param array $groupedColumns The grouping columns registered so far + * @param array $groupedTables The tables for which columns were registered so far + */ + protected function registerGroupColumns($alias, $table, array &$groupedColumns, array &$groupedTables) + { + switch ($table) { + case 'checktimeperiods': + $groupedColumns[] = 'ctp.timeperiod_id'; + break; + case 'contacts': + $groupedColumns[] = 'co.object_id'; + $groupedColumns[] = 'c.contact_id'; + break; + case 'hostobjects': + $groupedColumns[] = 'ho.object_id'; + break; + case 'hosts': + $groupedColumns[] = 'h.host_id'; + break; + case 'hostgroups': + $groupedColumns[] = 'hgo.object_id'; + $groupedColumns[] = 'hg.hostgroup_id'; + break; + case 'hoststatus': + $groupedColumns[] = 'hs.hoststatus_id'; + break; + case 'instances': + $groupedColumns[] = 'i.instance_id'; + break; + case 'servicegroups': + $groupedColumns[] = 'sgo.object_id'; + $groupedColumns[] = 'sg.servicegroup_id'; + break; + case 'serviceobjects': + $groupedColumns[] = 'so.object_id'; + break; + case 'serviceproblemsummary': + $groupedColumns[] = 'sps.unhandled_services_count'; + break; + case 'services': + $groupedColumns[] = 'so.object_id'; + $groupedColumns[] = 's.service_id'; + break; + case 'servicestatus': + $groupedColumns[] = 'ss.servicestatus_id'; + break; + default: + return; + } + + $groupedTables[$table] = true; + } + + /** + * {@inheritdoc} + */ + public function getGroup() + { + $group = parent::getGroup() ?: array(); + if (! is_array($group)) { + $group = array($group); + } + + $joinedOrigins = array_filter($this->groupOrigin, array($this, 'hasJoinedVirtualTable')); + if (empty($joinedOrigins)) { + return $group; + } + + $groupedTables = array(); + foreach ($this->groupBase as $baseTable => $aliasedPks) { + if (! $this->hasJoinedVirtualTable($baseTable)) { + continue; + } + $groupedTables[$baseTable] = true; + foreach ($aliasedPks as $aliasedPk) { + $group[] = $aliasedPk; + } + } + + foreach (new ColumnFilterIterator($this->columns) as $desiredAlias => $desiredColumn) { + $alias = is_string($desiredAlias) ? $this->customAliasToAlias($desiredAlias) : $desiredColumn; + if ($this->isCustomVar($alias) && $this->getDatasource()->getDbType() === 'pgsql') { + $table = $this->customVars[$alias]; + if (! isset($groupedTables[$table])) { + $group[] = $this->getCustomvarColumnName($alias); + $groupedTables[$table] = true; + } + continue; + } + $table = $this->aliasToTableName($alias); + if ($table && !isset($groupedTables[$table]) && ( + in_array($table, $joinedOrigins, true) || $this->getDatasource()->getDbType() === 'pgsql') + ) { + $this->registerGroupColumns($alias, $table, $group, $groupedTables); + } + } + + if (! empty($group) && $this->getDatasource()->getDbType() === 'pgsql') { + foreach (new ColumnFilterIterator($this->orderColumns) as $alias) { + if ($this->isCustomVar($alias)) { + $table = $this->customVars[$alias]; + if (! isset($groupedTables[$table])) { + $group[] = $this->getCustomvarColumnName($alias); + $groupedTables[$table] = true; + } + continue; + } + $table = $this->aliasToTableName($alias); + if ($table && !isset($groupedTables[$table]) + && !in_array($this->getMappedField($alias), $this->columns, true) + ) { + $this->registerGroupColumns($alias, $table, $group, $groupedTables); + } + } + } + + return array_unique($group); + } + + // TODO: Move this away, see note related to $idoVersion var + protected function getIdoVersion() + { + if (self::$idoVersion === null) { + $dbconf = $this->db->getConfig(); + $id = $dbconf['host'] . '/' . $dbconf['dbname']; + $session = null; + if (Icinga::app()->isWeb()) { + // TODO: Once we have version per connection we should choose a + // namespace based on resource name + $session = Session::getSession()->getNamespace('monitoring/ido/' . $id); + if (isset($session->version)) { + self::$idoVersion = $session->version; + return self::$idoVersion; + } + } + self::$idoVersion = $this->db->fetchOne( + $this->db->select()->from($this->prefix . 'dbversion', 'version') + ); + if ($session !== null) { + $session->version = self::$idoVersion; + } + } + return self::$idoVersion; + } + + /** + * Return the name of the primary key column for the given table name + * + * @param string $table + * + * @return string + * + * @throws ProgrammingError In case $table is unknown + */ + protected function getPrimaryKeyColumn($table) + { + // TODO: For god's sake, make this being a mapping + // (instead of matching a ton of properties using a ridiculous long switch case) + switch ($table) { + case 'instances': + return $this->instance_id; + case 'objects': + return $this->object_id; + case 'acknowledgements': + return $this->acknowledgement_id; + case 'commenthistory': + return $this->commenthistory_id; + case 'contactnotifiations': + return $this->contactnotification_id; + case 'downtimehistory': + return $this->downtimehistory_id; + case 'flappinghistory': + return $this->flappinghistory_id; + case 'notifications': + return $this->notification_id; + case 'statehistory': + return $this->statehistory_id; + case 'comments': + return $this->comment_id; + case 'customvariablestatus': + return $this->customvariablestatus_id; + case 'hoststatus': + return $this->hoststatus_id; + case 'programstatus': + return $this->programstatus_id; + case 'runtimevariables': + return $this->runtimevariable_id; + case 'scheduleddowntime': + return $this->scheduleddowntime_id; + case 'servicestatus': + return $this->servicestatus_id; + case 'contactstatus': + return $this->contactstatus_id; + case 'commands': + return $this->command_id; + case 'contactgroup_members': + return $this->contactgroup_member_id; + case 'contactgroups': + return $this->contactgroup_id; + case 'contacts': + return $this->contact_id; + case 'customvariables': + return $this->customvariable_id; + case 'host_contactgroups': + return $this->host_contactgroup_id; + case 'host_contacts': + return $this->host_contact_id; + case 'hostgroup_members': + return $this->hostgroup_member_id; + case 'hostgroups': + return $this->hostgroup_id; + case 'hosts': + return $this->host_id; + case 'service_contactgroups': + return $this->service_contactgroup_id; + case 'service_contacts': + return $this->service_contact_id; + case 'servicegroup_members': + return $this->servicegroup_member_id; + case 'servicegroups': + return $this->servicegroup_id; + case 'services': + return $this->service_id; + case 'timeperiods': + return $this->timeperiod_id; + default: + throw new ProgrammingError('Cannot provide a primary key column. Table "%s" is unknown', $table); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php new file mode 100644 index 0000000..ac538ec --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php @@ -0,0 +1,26 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class InstanceQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'instances' => array( + 'instance_id' => 'i.instance_id', + 'instance_name' => 'i.instance_name' + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select()->from(array('i' => $this->prefix . 'instances'), array()); + $this->joinedVirtualTables['instances'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php new file mode 100644 index 0000000..8bfb725 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php @@ -0,0 +1,144 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service notifications + */ +class NotificationQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'notifications' => array( + 'id' => 'n.id', + 'instance_name' => 'n.instance_name', + 'notification_contact_name' => 'n.notification_contact_name', + 'notification_output' => 'n.notification_output', + 'notification_reason' => 'n.notification_reason', + 'notification_state' => 'n.notification_state', + 'notification_timestamp' => 'n.notification_timestamp' + ), + 'hosts' => array( + 'host_display_name' => 'n.host_display_name', + 'host_name' => 'n.host_name' + ), + 'services' => array( + 'service_description' => 'n.service_description', + 'service_display_name' => 'n.service_display_name', + 'service_host_name' => 'n.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $notificationQuery; + + /** + * Subqueries used for the notification query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->notificationQuery = $this->db->select(); + $this->select->from( + array('n' => $this->notificationQuery), + array() + ); + $this->joinedVirtualTables['notifications'] = true; + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = $this->desiredColumns; + $columns = array_combine($columns, $columns); + foreach ($this->columnMap['services'] as $column => $_) { + if (isset($columns[$column])) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + } + $hosts = $this->createSubQuery('hostnotification', $columns); + $hosts->setIsSubQuery(true); + $this->subQueries[] = $hosts; + $this->notificationQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $services = $this->createSubQuery('servicenotification', $this->desiredColumns); + $services->setIsSubQuery(true); + $this->subQueries[] = $services; + $this->notificationQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php new file mode 100644 index 0000000..87a71f6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php @@ -0,0 +1,52 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host and service notification events + */ +class NotificationeventQuery extends IdoQuery +{ + protected $columnMap = array( + 'notificationevent' => array( + 'notificationevent_id' => 'n.notification_id', + 'notificationevent_reason' => <<<EOF +(CASE n.notification_reason + WHEN 0 THEN 'normal_notification' + WHEN 1 THEN 'ack' + WHEN 2 THEN 'flapping_started' + WHEN 3 THEN 'flapping_stopped' + WHEN 4 THEN 'flapping_disabled' + WHEN 5 THEN 'dt_start' + WHEN 6 THEN 'dt_end' + WHEN 7 THEN 'dt_cancel' + WHEN 99 THEN 'custom_notification' + ELSE NULL +END) +EOF + , + 'notificationevent_start_time' => 'UNIX_TIMESTAMP(n.start_time)', + 'notificationevent_end_time' => 'UNIX_TIMESTAMP(n.end_time)', + 'notificationevent_state' => 'n.state', + 'notificationevent_output' => 'n.output', + 'notificationevent_long_output' => 'n.long_output', + 'notificationevent_escalated' => 'n.escalated', + 'notificationevent_contacts_notified' => 'n.contacts_notified' + ), + 'object' => array( + 'host_name' => 'o.name1', + 'service_description' => 'o.name2' + ) + ); + + protected function joinBaseTables() + { + $this->select() + ->from(array('n' => $this->prefix . 'notifications'), array()) + ->join(array('o' => $this->prefix . 'objects'), 'n.object_id = o.object_id', array()); + + $this->joinedVirtualTables['notificationevent'] = true; + $this->joinedVirtualTables['object'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php new file mode 100644 index 0000000..f629115 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php @@ -0,0 +1,142 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service notification history + */ +class NotificationhistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'history' => array( + 'id' => 'n.id', + 'object_type' => 'n.object_type', + 'output' => 'n.output', + 'state' => 'n.state', + 'timestamp' => 'n.timestamp', + 'type' => 'n.type' + ), + 'hosts' => array( + 'host_display_name' => 'n.host_display_name', + 'host_name' => 'n.host_name' + ), + 'services' => array( + 'service_description' => 'n.service_description', + 'service_display_name' => 'n.service_display_name', + 'service_host_name' => 'n.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $notificationQuery; + + /** + * Subqueries used for the notification query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->notificationQuery = $this->db->select(); + $this->select->from( + array('n' => $this->notificationQuery), + array() + ); + $this->joinedVirtualTables['history'] = true; + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = $this->desiredColumns; + $columns = array_combine($columns, $columns); + foreach ($this->columnMap['services'] as $column => $_) { + if (isset($columns[$column])) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + } + $hosts = $this->createSubQuery('hostnotification', $columns); + $this->subQueries[] = $hosts; + $this->notificationQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_flip($this->desiredColumns); + $services = $this->createSubQuery('servicenotification', array_flip($columns)); + $this->subQueries[] = $services; + $this->notificationQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php new file mode 100644 index 0000000..9e9f5f6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php @@ -0,0 +1,68 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Program status query + */ +class ProgramstatusQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'programstatus' => array( + 'id' => 'programstatus_id', + 'status_update_time' => 'UNIX_TIMESTAMP(programstatus.status_update_time)', + 'program_version' => 'program_version', + 'program_start_time' => 'UNIX_TIMESTAMP(programstatus.program_start_time)', + 'program_end_time' => 'UNIX_TIMESTAMP(programstatus.program_end_time)', + 'is_currently_running' => 'CASE WHEN (UNIX_TIMESTAMP(programstatus.status_update_time) + 60 > UNIX_TIMESTAMP(NOW())) + THEN + 1 + ELSE + 0 + END', + 'process_id' => 'process_id', + 'endpoint_name' => 'endpoint_name', + 'daemon_mode' => 'daemon_mode', + 'last_command_check' => 'UNIX_TIMESTAMP(programstatus.last_command_check)', + 'last_log_rotation' => 'UNIX_TIMESTAMP(programstatus.last_log_rotation)', + 'notifications_enabled' => 'notifications_enabled', + 'disable_notif_expire_time' => 'UNIX_TIMESTAMP(programstatus.disable_notif_expire_time)', + 'active_service_checks_enabled' => 'active_service_checks_enabled', + 'passive_service_checks_enabled' => 'passive_service_checks_enabled', + 'active_host_checks_enabled' => 'active_host_checks_enabled', + 'passive_host_checks_enabled' => 'passive_host_checks_enabled', + 'event_handlers_enabled' => 'event_handlers_enabled', + 'flap_detection_enabled' => 'flap_detection_enabled', + 'failure_prediction_enabled' => 'failure_prediction_enabled', + 'process_performance_data' => 'process_performance_data', + 'obsess_over_hosts' => 'obsess_over_hosts', + 'obsess_over_services' => 'obsess_over_services', + 'modified_host_attributes' => 'modified_host_attributes', + 'modified_service_attributes' => 'modified_service_attributes', + 'global_host_event_handler' => 'global_host_event_handler', + 'global_service_event_handler' => 'global_service_event_handler', + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + parent::joinBaseTables(); + + if (version_compare($this->getIdoVersion(), '1.11.7', '<')) { + $this->columnMap['programstatus']['endpoint_name'] = '(0)'; + } + if (version_compare($this->getIdoVersion(), '1.11.8', '<')) { + $this->columnMap['programstatus']['program_version'] = '(NULL)'; + } + if (version_compare($this->getIdoVersion(), '1.8', '<')) { + $this->columnMap['programstatus']['disable_notif_expire_time'] = '(NULL)'; + } + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php new file mode 100644 index 0000000..1aa2257 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php @@ -0,0 +1,80 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Zend_Db_Select; + +/** + * Query check summaries out of database + */ +class RuntimesummaryQuery extends IdoQuery +{ + protected $columnMap = array( + 'runtimesummary' => array( + 'check_type' => 'check_type', + 'active_checks_enabled' => 'active_checks_enabled', + 'passive_checks_enabled' => 'passive_checks_enabled', + 'execution_time' => 'execution_time', + 'latency' => 'latency', + 'object_count' => 'object_count', + 'object_type' => 'object_type' + ) + ); + + protected function joinBaseTables() + { + $hosts = $this->db->select()->from( + array('ho' => $this->prefix . 'objects'), + array() + )->join( + array('hs' => $this->prefix . 'hoststatus'), + 'ho.object_id = hs.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + array() + )->columns( + array( + 'check_type' => 'CASE ' + . 'WHEN hs.active_checks_enabled = 0 AND hs.passive_checks_enabled = 1 THEN \'passive\' ' + . 'WHEN hs.active_checks_enabled = 1 THEN \'active\' ' + . 'END', + 'active_checks_enabled' => 'hs.active_checks_enabled', + 'passive_checks_enabled' => 'hs.passive_checks_enabled', + 'execution_time' => 'SUM(hs.execution_time)', + 'latency' => 'SUM(hs.latency)', + 'object_count' => 'COUNT(*)', + 'object_type' => "('host')" + ) + )->group('check_type')->group('active_checks_enabled')->group('passive_checks_enabled'); + + $services = $this->db->select()->from( + array('so' => $this->prefix . 'objects'), + array() + )->join( + array('ss' => $this->prefix . 'servicestatus'), + 'so.object_id = ss.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + )->columns( + array( + 'check_type' => 'CASE ' + . 'WHEN ss.active_checks_enabled = 0 AND ss.passive_checks_enabled = 1 THEN \'passive\' ' + . 'WHEN ss.active_checks_enabled = 1 THEN \'active\' ' + . 'END', + 'active_checks_enabled' => 'ss.active_checks_enabled', + 'passive_checks_enabled' => 'ss.passive_checks_enabled', + 'execution_time' => 'SUM(ss.execution_time)', + 'latency' => 'SUM(ss.latency)', + 'object_count' => 'COUNT(*)', + 'object_type' => "('service')" + ) + )->group('check_type')->group('active_checks_enabled')->group('passive_checks_enabled'); + + $union = $this->db->select()->union( + array('s' => $services, 'h' => $hosts), + Zend_Db_Select::SQL_UNION_ALL + ); + + $this->select->from(array('hs' => $union)); + + $this->joinedVirtualTables = array('runtimesummary' => true); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php new file mode 100644 index 0000000..494744a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php @@ -0,0 +1,18 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for runtimevariables table + */ +class RuntimevariablesQuery extends IdoQuery +{ + protected $columnMap = array( + 'runtimevariables' => array( + 'id' => 'runtimevariable_id', + 'varname' => 'varname', + 'varvalue' => 'varvalue' + ) + ); +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php new file mode 100644 index 0000000..cae11bc --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php @@ -0,0 +1,218 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for service comments + */ +class ServicecommentQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('comments' => array('c.comment_id', 'so.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'comments' => array( + 'comment_author' => 'c.author_name COLLATE latin1_general_ci', + 'comment_author_name' => 'c.author_name', + 'comment_data' => 'c.comment_data', + 'comment_expiration' => 'CASE c.expires WHEN 1 THEN UNIX_TIMESTAMP(c.expiration_time) ELSE NULL END', + 'comment_internal_id' => 'c.internal_comment_id', + 'comment_is_persistent' => 'c.is_persistent', + 'comment_name' => 'c.name', + 'comment_timestamp' => 'UNIX_TIMESTAMP(c.comment_time)', + 'comment_type' => "CASE c.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'downtime' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END", + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_type' => '(\'service\')', + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host' => 'so.name1 COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'hoststatus' => array( + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ), + 'servicestatus' => array( + 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END' + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.14.0', '<')) { + $this->columnMap['comments']['comment_name'] = '(NULL)'; + } + $this->select->from( + array('c' => $this->prefix . 'comments'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = c.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + $this->joinedVirtualTables['comments'] = true; + } + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sg.' . $this->servicegroup_id, + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = c.instance_id', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + /** + * Join service status + */ + protected function joinServicestatus() + { + $this->select->join( + array('ss' => $this->prefix . 'servicestatus'), + 'ss.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $this->requireVirtualTable('services'); + + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 's.host_object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php new file mode 100644 index 0000000..33aaa25 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php @@ -0,0 +1,44 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service comment removal records + */ +class ServicecommentdeletionhistoryQuery extends ServicecommenthistoryQuery +{ + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('sch.deletion_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + parent::joinBaseTables(); + $this->select->where("sch.deletion_time > '1970-01-02 00:00:00'"); + $this->columnMap['commenthistory']['timestamp'] = str_replace( + 'comment_time', + 'deletion_time', + $this->columnMap['commenthistory']['timestamp'] + ); + $this->columnMap['commenthistory']['type'] = str_replace( + 'END)', + "END || '_deleted')", + $this->columnMap['commenthistory']['type'] + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php new file mode 100644 index 0000000..b3e9c16 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php @@ -0,0 +1,195 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service comment history records + */ +class ServicecommenthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('commenthistory' => array('sch.commenthistory_id', 'so.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups', 'services'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'commenthistory' => array( + 'id' => 'sch.commenthistory_id', + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_id' => 'sch.object_id', + 'object_type' => '(\'service\')', + 'output' => "('[' || sch.author_name || '] ' || sch.comment_data)", + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host' => 'so.name1 COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1', + 'state' => '(-1)', + 'timestamp' => 'UNIX_TIMESTAMP(sch.comment_time)', + 'type' => "(CASE sch.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'dt_comment' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END)" + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('sch.comment_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('sch' => $this->prefix . 'commenthistory'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sch.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + $this->joinedVirtualTables['commenthistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sch.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('services'); + + return ['so.object_id', 'so.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php new file mode 100644 index 0000000..0a46709 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php @@ -0,0 +1,235 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for service contacts + */ +class ServicecontactQuery extends IdoQuery +{ + protected $allowCustomVars = true; + + protected $groupBase = [ + 'contacts' => ['co.object_id', 'c.contact_id'], + 'timeperiods' => ['ht.timeperiod_id', 'st.timeperiod_id'] + ]; + + protected $groupOrigin = ['contactgroups', 'hosts', 'services']; + + protected $subQueryTargets = [ + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ]; + + protected $columnMap = [ + 'contactgroups' => [ + 'contactgroup' => 'cgo.name1 COLLATE latin1_general_ci', + 'contactgroup_name' => 'cgo.name1', + 'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci' + ], + 'contacts' => [ + 'contact_id' => 'c.contact_id', + 'contact' => 'co.name1 COLLATE latin1_general_ci', + 'contact_name' => 'co.name1', + 'contact_alias' => 'c.alias COLLATE latin1_general_ci', + 'contact_email' => 'c.email_address COLLATE latin1_general_ci', + 'contact_pager' => 'c.pager_address', + 'contact_object_id' => 'c.contact_object_id', + 'contact_has_host_notfications' => 'c.host_notifications_enabled', + 'contact_has_service_notfications' => 'c.service_notifications_enabled', + 'contact_can_submit_commands' => 'c.can_submit_commands', + 'contact_notify_service_recovery' => 'c.notify_service_recovery', + 'contact_notify_service_warning' => 'c.notify_service_warning', + 'contact_notify_service_critical' => 'c.notify_service_critical', + 'contact_notify_service_unknown' => 'c.notify_service_unknown', + 'contact_notify_service_flapping' => 'c.notify_service_flapping', + 'contact_notify_service_downtime' => 'c.notify_service_downtime', + 'contact_notify_host_recovery' => 'c.notify_host_recovery', + 'contact_notify_host_down' => 'c.notify_host_down', + 'contact_notify_host_unreachable' => 'c.notify_host_unreachable', + 'contact_notify_host_flapping' => 'c.notify_host_flapping', + 'contact_notify_host_downtime' => 'c.notify_host_downtime' + ], + 'hostgroups' => [ + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ], + 'hosts' => [ + 'host' => 'ho.name1 COLLATE latin1_general_ci', + 'host_name' => 'ho.name1', + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ], + 'instances' => [ + 'instance_name' => 'i.instance_name' + ], + 'servicegroups' => [ + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ], + 'services' => [ + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ], + 'timeperiods' => [ + 'contact_notify_host_timeperiod' => 'ht.alias COLLATE latin1_general_ci', + 'contact_notify_service_timeperiod' => 'st.alias COLLATE latin1_general_ci' + ] + ]; + + protected function joinBaseTables() + { + $this->select->from( + ['c' => $this->prefix . 'contacts'], + [] + )->join( + ['co' => $this->prefix . 'objects'], + 'co.object_id = c.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10', + [] + ); + + $this->select->joinLeft( + ['sc' => $this->prefix . 'service_contacts'], + 'sc.contact_object_id = c.contact_object_id', + [] + )->joinLeft( + ['s' => $this->prefix . 'services'], + 's.service_id = sc.service_id', + [] + )->joinLeft( + ['so' => $this->prefix . 'objects'], + 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2', + [] + ); + + $this->joinedVirtualTables['contacts'] = true; + $this->joinedVirtualTables['services'] = true; + } + + /** + * Join contact groups + */ + protected function joinContactgroups() + { + $this->select->joinLeft( + ['cgm' => $this->prefix . 'contactgroup_members'], + 'co.object_id = cgm.contact_object_id', + [] + )->joinLeft( + ['cg' => $this->prefix . 'contactgroups'], + 'cgm.contactgroup_id = cg.contactgroup_id', + [] + )->joinLeft( + ['cgo' => $this->prefix . 'objects'], + 'cg.contactgroup_object_id = cgo.object_id AND cgo.is_active = 1 AND cgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('hosts'); + $this->select->joinLeft( + ['hgm' => $this->prefix . 'hostgroup_members'], + 'hgm.host_object_id = ho.object_id', + [] + )->joinLeft( + ['hg' => $this->prefix . 'hostgroups'], + 'hg.hostgroup_id = hgm.hostgroup_id', + [] + )->joinLeft( + ['hgo' => $this->prefix . 'objects'], + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + [] + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->joinLeft( + ['h' => $this->prefix . 'hosts'], + 'h.host_object_id = s.host_object_id', + [] + )->joinLeft( + ['ho' => $this->prefix . 'objects'], + 'ho.object_id = h.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1', + [] + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + ['i' => $this->prefix . 'instances'], + 'i.instance_id = c.instance_id', + [] + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + ['sgm' => $this->prefix . 'servicegroup_members'], + 'sgm.service_object_id = s.service_object_id', + [] + )->joinLeft( + ['sg' => $this->prefix . 'servicegroups'], + 'sg.servicegroup_id = sgm.servicegroup_id', + [] + )->joinLeft( + ['sgo' => $this->prefix . 'objects'], + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + [] + ); + } + + /** + * Join time periods + */ + protected function joinTimeperiods() + { + $this->select->joinLeft( + ['ht' => $this->prefix . 'timeperiods'], + 'ht.timeperiod_object_id = c.host_timeperiod_object_id', + [] + ); + $this->select->joinLeft( + ['st' => $this->prefix . 'timeperiods'], + 'st.timeperiod_object_id = c.service_timeperiod_object_id', + [] + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 's.host_object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php new file mode 100644 index 0000000..feea061 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php @@ -0,0 +1,222 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for service downtimes + */ +class ServicedowntimeQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('downtimes' => array('sd.scheduleddowntime_id', 'so.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimes' => array( + 'downtime_author' => 'sd.author_name COLLATE latin1_general_ci', + 'downtime_author_name' => 'sd.author_name', + 'downtime_comment' => 'sd.comment_data', + 'downtime_duration' => 'sd.duration', + 'downtime_end' => 'CASE WHEN sd.is_fixed > 0 THEN UNIX_TIMESTAMP(sd.scheduled_end_time) ELSE UNIX_TIMESTAMP(sd.trigger_time) + sd.duration END', + 'downtime_entry_time' => 'UNIX_TIMESTAMP(sd.entry_time)', + 'downtime_internal_id' => 'sd.internal_downtime_id', + 'downtime_is_fixed' => 'sd.is_fixed', + 'downtime_is_flexible' => 'CASE WHEN sd.is_fixed = 0 THEN 1 ELSE 0 END', + 'downtime_is_in_effect' => 'sd.is_in_effect', + 'downtime_name' => 'sd.name', + 'downtime_scheduled_end' => 'UNIX_TIMESTAMP(sd.scheduled_end_time)', + 'downtime_scheduled_start' => 'UNIX_TIMESTAMP(sd.scheduled_start_time)', + 'downtime_start' => 'UNIX_TIMESTAMP(CASE WHEN UNIX_TIMESTAMP(sd.trigger_time) > 0 then sd.trigger_time ELSE sd.scheduled_start_time END)', + 'downtime_triggered_by_id' => 'sd.triggered_by_id', + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_type' => '(\'service\')', + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host' => 'so.name1 COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1' + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'hoststatus' => array( + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ), + 'servicestatus' => array( + 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END' + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.14.0', '<')) { + $this->columnMap['downtimes']['downtime_name'] = '(NULL)'; + } + $this->select->from( + array('sd' => $this->prefix . 'scheduleddowntime'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'sd.object_id = so.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + $this->joinedVirtualTables['downtimes'] = true; + } + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sd.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sgm.servicegroup_id = sg.' . $this->servicegroup_id, + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + /** + * Join service status + */ + protected function joinServicestatus() + { + $this->select->join( + array('ss' => $this->prefix . 'servicestatus'), + 'ss.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('services'); + + return ['so.object_id', 'so.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php new file mode 100644 index 0000000..2d592c8 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for host downtime end history records + */ +class ServicedowntimeendhistoryQuery extends ServicedowntimestarthistoryQuery +{ + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('sdh.actual_end_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + parent::joinBaseTables(true); + $this->select->where("sdh.actual_end_time > '1970-01-02 00:00:00'"); + $this->columnMap['downtimehistory']['type'] = "('dt_end')"; + $this->columnMap['downtimehistory']['timestamp'] = str_replace( + 'actual_start_time', + 'actual_end_time', + $this->columnMap['downtimehistory']['timestamp'] + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php new file mode 100644 index 0000000..f22e265 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php @@ -0,0 +1,202 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service downtime start history records + */ +class ServicedowntimestarthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('downtimehistory' => array('sdh.downtimehistory_id', 'so.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'downtimehistory' => array( + 'id' => 'sdh.downtimehistory_id', + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_id' => 'sdh.object_id', + 'object_type' => '(\'service\')', + 'output' => "('[' || sdh.author_name || '] ' || sdh.comment_data)", + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host' => 'so.name1 COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1', + 'state' => '(-1)', + 'timestamp' => 'UNIX_TIMESTAMP(sdh.actual_start_time)', + 'type' => "('dt_start')" + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('sdh.actual_start_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('sdh' => $this->prefix . 'downtimehistory'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sdh.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + + if (func_num_args() === 0 || func_get_arg(0) === false) { + $this->select->where( + "sdh.actual_start_time > '1970-01-02 00:00:00'" + ); + } + + $this->joinedVirtualTables['downtimehistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sdh.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('services'); + + return ['so.object_id', 'so.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php new file mode 100644 index 0000000..48fb0bc --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for service flapping end history records + */ +class ServiceflappingendhistoryQuery extends ServiceflappingstarthistoryQuery +{ + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('sfh' => $this->prefix . 'flappinghistory'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sfh.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + + $this->select->where('sfh.event_type = 1001'); + + $this->joinedVirtualTables['flappinghistory'] = true; + + $this->columnMap['flappinghistory']['type'] = '(\'flapping_deleted\')'; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php new file mode 100644 index 0000000..f068681 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php @@ -0,0 +1,197 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service flapping start history records + */ +class ServiceflappingstarthistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('flappinghistory' => array('sfh.flappinghistory_id', 'so.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'flappinghistory' => array( + 'id' => 'sfh.flappinghistory_id', + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_id' => 'sfh.object_id', + 'object_type' => '(\'service\')', + 'output' => '(sfh.percent_state_change || \'\')', + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host_name' => 'so.name1', + 'state' => '(-1)', + 'timestamp' => 'UNIX_TIMESTAMP(sfh.event_time)', + 'type' => "('flapping')" + ), + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup_name' => 'sgo.name1', + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') { + $this->requireColumn('timestamp'); + $filter->setColumn('sfh.event_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('sfh' => $this->prefix . 'flappinghistory'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sfh.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + + $this->select->where('sfh.event_type = 1000'); + + $this->joinedVirtualTables['flappinghistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sfh.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('services'); + + return ['so.object_id', 'so.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php new file mode 100644 index 0000000..7f7be50 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php @@ -0,0 +1,303 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +class ServicegroupQuery extends IdoQuery +{ + protected $groupBase = array( + 'servicegroups' => array('sgo.object_id', 'sg.servicegroup_id'), + 'servicestatus' => array('ss.servicestatus_id', 'hs.hoststatus_id') + ); + + protected $groupOrigin = array('members'); + + protected $allowCustomVars = true; + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + protected $columnMap = array( + 'contacts' => [ + 'service_contact' => 'sco.name1' + ], + 'contactgroups' => [ + 'service_contactgroup' => 'scgo.name1' + ], + 'hostcontacts' => [ + 'host_contact' => 'hco.name1' + ], + 'hostcontactgroups' => [ + 'host_contactgroup' => 'hcgo.name1' + ], + 'hostgroups' => array( + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'h.host_object_id' => 's.host_object_id' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'members' => array( + 'host_name' => 'so.name1', + 'service_description' => 'so.name2' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1' + ), + 'servicestatus' => array( + 'service_handled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) > 0 THEN 1 ELSE 0 END', + 'service_severity' => ' + CASE WHEN ss.current_state = 0 + THEN + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL + THEN 16 + ELSE 0 + END + + + CASE WHEN ss.problem_has_been_acknowledged = 1 + THEN 2 + ELSE + CASE WHEN ss.scheduled_downtime_depth > 0 + THEN 1 + ELSE 4 + END + END + ELSE + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 16 + WHEN ss.current_state = 1 THEN 32 + WHEN ss.current_state = 2 THEN 128 + WHEN ss.current_state = 3 THEN 64 + ELSE 256 + END + + + CASE WHEN hs.current_state > 0 + THEN 1024 + ELSE + CASE WHEN ss.problem_has_been_acknowledged = 1 + THEN 512 + ELSE + CASE WHEN ss.scheduled_downtime_depth > 0 + THEN 256 + ELSE 2048 + END + END + END + END', + 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END' + ) + ); + + protected function joinBaseTables() + { + $this->select->from( + array('sgo' => $this->prefix . 'objects'), + array() + )->join( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.servicegroup_object_id = sgo.object_id AND sgo.objecttype_id = 4 AND sgo.is_active = 1', + array() + ); + $this->joinedVirtualTables = array('servicegroups' => true); + } + + /** + * Join contacts + */ + protected function joinContacts() + { + $this->requireVirtualTable('services'); + + $this->select->joinLeft( + ['sc' => 'icinga_service_contacts'], + 'sc.service_id = s.service_id', + [] + )->joinLeft( + ['sco' => 'icinga_objects'], + 'sco.object_id = sc.contact_object_id AND sco.is_active = 1 AND sco.objecttype_id = 10', + [] + ); + } + + /** + * Join contact groups + */ + protected function joinContactgroups() + { + $this->requireVirtualTable('services'); + + $this->select->joinLeft( + ['scg' => 'icinga_service_contactgroups'], + 'scg.service_id = s.service_id', + [] + )->joinLeft( + ['scgo' => 'icinga_objects'], + 'scgo.object_id = scg.contactgroup_object_id AND scgo.is_active = 1 AND scgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host contacts + */ + protected function joinHostcontacts() + { + $this->requireVirtualTable('services'); + + $this->select->joinLeft( + ['h' => 'icinga_hosts'], + 'h.host_object_id = s.host_object_id', + [] + )->joinLeft( + ['hc' => 'icinga_host_contacts'], + 'hc.host_id = h.host_id', + [] + )->joinLeft( + ['hco' => 'icinga_objects'], + 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10', + [] + ); + } + + /** + * Join host contact groups + */ + protected function joinHostcontactgroups() + { + $this->requireVirtualTable('services'); + + $this->select->joinLeft( + ['h' => 'icinga_hosts'], + 'h.host_object_id = s.host_object_id', + [] + )->joinLeft( + ['hcg' => 'icinga_host_contactgroups'], + 'hcg.host_id = h.host_id', + [] + )->joinLeft( + ['hcgo' => 'icinga_objects'], + 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.objecttype_id = 3 AND hgo.is_active = 1', + array() + ); + } + + /** + * Join hosts + * + * This is required to make filters work which filter by host custom variables. + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + + // Host custom var filters work w/o any host related table. If a host table join is necessary here some day, + // please adjust `joinHostcontact*()` where we explicitly do this already + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sg.instance_id', + array() + ); + } + + /** + * Join service objects + */ + protected function joinMembers() + { + $this->select->join( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.servicegroup_id = sg.servicegroup_id', + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sgm.service_object_id AND so.objecttype_id = 2 AND so.is_active = 1', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->requireVirtualTable('members'); + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + /** + * Join service status + */ + protected function joinServicestatus() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = s.host_object_id', + array() + ); + $this->select->join( + array('ss' => $this->prefix . 'servicestatus'), + 'ss.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $this->requireVirtualTable('members'); + + $query->joinVirtualTable('services'); + + return ['so.object_id', 'so.object_id']; + } elseif ($name === 'servicegroup') { + // Propagate that the "parent" query has to be filtered as well + $additionalFilter = clone $filter; + + $this->requireVirtualTable('members'); + + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php new file mode 100644 index 0000000..11b62d0 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php @@ -0,0 +1,113 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Zend_Db_Expr; +use Zend_Db_Select; + +/** + * Query for service group summary + */ +class ServicegroupsummaryQuery extends IdoQuery +{ + + protected $allowCustomVars = true; + + protected $columnMap = array( + 'servicegroupsummary' => array( + 'servicegroup_alias' => 'servicegroup_alias', + 'servicegroup_name' => 'servicegroup_name', + 'services_critical' => 'SUM(CASE WHEN service_state = 2 THEN 1 ELSE 0 END)', + 'services_critical_handled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 1 THEN 1 ELSE 0 END)', + 'services_critical_unhandled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 0 THEN 1 ELSE 0 END)', + 'services_ok' => 'SUM(CASE WHEN service_state = 0 THEN 1 ELSE 0 END)', + 'services_pending' => 'SUM(CASE WHEN service_state = 99 THEN 1 ELSE 0 END)', + 'services_severity' => 'MAX(service_severity)', + 'services_total' => 'SUM(1)', + 'services_unknown' => 'SUM(CASE WHEN service_state = 3 THEN 1 ELSE 0 END)', + 'services_unknown_handled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 1 THEN 1 ELSE 0 END)', + 'services_unknown_unhandled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 0 THEN 1 ELSE 0 END)', + 'services_warning' => 'SUM(CASE WHEN service_state = 1 THEN 1 ELSE 0 END)', + 'services_warning_handled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 1 THEN 1 ELSE 0 END)', + 'services_warning_unhandled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 0 THEN 1 ELSE 0 END)', + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $summaryQuery; + + /** + * Subqueries used for the summary query + * + * @var IdoQuery[] + */ + protected $subQueries = []; + + /** + * Count query + * + * @var IdoQuery + */ + protected $countQuery; + + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + $this->countQuery->applyFilter(clone $filter); + return $this; + } + + protected function joinBaseTables() + { + $this->countQuery = $this->createSubQuery( + 'Servicegroup', + array() + ); + $subQuery = $this->createSubQuery( + 'Servicegroup', + array( + 'servicegroup_alias', + 'servicegroup_name', + 'service_handled', + 'service_severity', + 'service_state' + ) + ); + $this->subQueries[] = $subQuery; + $emptyGroups = $this->createSubQuery( + 'Emptyservicegroup', + [ + 'servicegroup_alias', + 'servicegroup_name', + 'service_handled' => new Zend_Db_Expr('NULL'), + 'service_severity' => new Zend_Db_Expr('0'), + 'service_state' => new Zend_Db_Expr('NULL'), + ] + ); + $this->subQueries[] = $emptyGroups; + $this->summaryQuery = $this->db->select()->union( + [$subQuery, $emptyGroups], + Zend_Db_Select::SQL_UNION_ALL + ); + $this->select->from(['servicesgroupsummary' => $this->summaryQuery], []); + $this->group(['servicegroup_name', 'servicegroup_alias']); + $this->joinedVirtualTables['servicegroupsummary'] = true; + } + + public function getCountQuery() + { + $count = $this->countQuery->select(); + $this->countQuery->applyFilterSql($count); + $count->columns(array('sgo.object_id')); + $count->group(array('sgo.object_id')); + return $this->db->select()->from($count, array('cnt' => 'COUNT(*)')); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php new file mode 100644 index 0000000..d3fccf0 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php @@ -0,0 +1,286 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service notifications + */ +class ServicenotificationQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'contactnotifications' => array( + 'notification_contact_name' => 'co.name1' + ), + 'history' => array( + 'output' => null, + 'state' => 'sn.state', + 'timestamp' => 'UNIX_TIMESTAMP(sn.start_time)', + 'type' => ' + CASE sn.notification_reason + WHEN 1 THEN \'notification_ack\' + WHEN 2 THEN \'notification_flapping\' + WHEN 3 THEN \'notification_flapping_end\' + WHEN 5 THEN \'notification_dt_start\' + WHEN 6 THEN \'notification_dt_end\' + WHEN 7 THEN \'notification_dt_end\' + WHEN 8 THEN \'notification_custom\' + ELSE \'notification_state\' + END', + ), + 'hostgroups' => array( + 'hostgroup_name' => 'hgo.name1', + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + ), + 'hosts' => array( + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci', + 'host_alias' => 'h.alias COLLATE latin1_general_ci', + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'notifications' => array( + 'id' => 'sn.notification_id', + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'notification_output' => 'sn.output', + 'notification_reason' => 'sn.notification_reason', + 'notification_state' => 'sn.state', + 'notification_timestamp' => 'UNIX_TIMESTAMP(sn.start_time)', + 'object_type' => '(\'service\')', + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host_name' => 'so.name1' + ), + 'servicegroups' => array( + 'servicegroup_name' => 'sgo.name1', + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ) + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression) { + switch ($filter->getColumn()) { + case 'output': + $this->requireColumn('output'); + $filter->setColumn('sn.output'); + return null; + case 'timestamp': + case 'notification_timestamp': + $this->requireColumn($filter->getColumn()); + $filter->setColumn('sn.start_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + } + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + switch ($this->ds->getDbType()) { + case 'mysql': + $concattedContacts = "GROUP_CONCAT(" + . "DISTINCT co.name1 ORDER BY co.name1 SEPARATOR ', '" + . ") COLLATE latin1_general_ci"; + break; + case 'pgsql': + // TODO: Find a way to order the contact alias list: + $concattedContacts = "ARRAY_TO_STRING(ARRAY_AGG(DISTINCT co.name1), ', ')"; + break; + } + $this->columnMap['history']['output'] = "('[' || $concattedContacts || '] ' || sn.output)"; + + $this->select->from( + array('sn' => $this->prefix . 'notifications'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sn.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + $this->joinedVirtualTables['notifications'] = true; + } + + /** + * Join virtual table history + */ + protected function joinHistory() + { + $this->requireVirtualTable('contactnotifications'); + } + + /** + * Join contact notifications + */ + protected function joinContactnotifications() + { + $this->select->joinLeft( + array('cn' => $this->prefix . 'contactnotifications'), + 'cn.notification_id = sn.notification_id', + array() + ); + $this->select->joinLeft( + array('co' => $this->prefix . 'objects'), + 'co.object_id = cn.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10', + array() + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sn.instance_id', + array() + ); + } + + /** + * {@inheritdoc} + */ + public function getGroup() + { + $group = array(); + + if ($this->hasJoinedVirtualTable('history') + || $this->hasJoinedVirtualTable('hostgroups') + || $this->hasJoinedVirtualTable('servicegroups') + ) { + $group = array('sn.notification_id', 'so.object_id'); + if ($this->hasJoinedVirtualTable('contactnotifications') && !$this->hasJoinedVirtualTable('history')) { + $group[] = 'co.object_id'; + } + } elseif ($this->hasJoinedVirtualTable('contactnotifications')) { + $group = array('sn.notification_id', 'co.object_id', 'so.object_id'); + } + + if (! empty($group)) { + if ($this->hasJoinedVirtualTable('hosts')) { + $group[] = 'h.host_id'; + } + + if ($this->hasJoinedVirtualTable('services')) { + $group[] = 's.service_id'; + } + + if ($this->hasJoinedVirtualTable('instances')) { + $group[] = 'i.instance_id'; + } + } + + return $group; + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $this->requireVirtualTable('services'); + + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 's.host_object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php new file mode 100644 index 0000000..f93ca8a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php @@ -0,0 +1,220 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service state history records + */ +class ServicestatehistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('statehistory' => array('sh.statehistory_id', 'so.object_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups'); + + /** + * Array to map type names to type ids for query optimization + * + * @var array + */ + protected $types = array( + 'soft_state' => 0, + 'hard_state' => 1 + ); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_alias' => 'h.alias', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci' + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'services' => array( + 'service_display_name' => 's.display_name COLLATE latin1_general_ci' + ), + 'statehistory' => array( + 'id' => 'sh.statehistory_id', + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_id' => 'sh.object_id', + 'object_type' => '(\'service\')', + 'output' => '(CASE WHEN sh.state_type = 1 THEN sh.output ELSE \'[ \' || sh.current_check_attempt || \'/\' || sh.max_check_attempts || \' ] \' || sh.output END)', + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_description' => 'so.name2', + 'service_host' => 'so.name1 COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1', + 'state' => 'sh.state', + 'timestamp' => 'UNIX_TIMESTAMP(sh.state_time)', + 'type' => "(CASE WHEN sh.state_type = 1 THEN 'hard_state' ELSE 'soft_state' END)" + ), + ); + + protected function requireFilterColumns(Filter $filter) + { + if ($filter instanceof FilterExpression) { + switch ($filter->getColumn()) { + case 'timestamp': + $this->requireColumn('timestamp'); + $filter->setColumn('sh.state_time'); + $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression()))); + return null; + case 'type': + if (! is_array($filter->getExpression())) { + $this->requireColumn('type'); + $filter->setColumn('sh.state_type'); + if (isset($this->types[$filter->getExpression()])) { + $filter->setExpression($this->types[$filter->getExpression()]); + } else { + $filter->setExpression(-1); + } + + return null; + } + } + } + + return parent::requireFilterColumns($filter); + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->select->from( + array('sh' => $this->prefix . 'statehistory'), + array() + )->join( + array('so' => $this->prefix . 'objects'), + 'so.object_id = sh.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + $this->joinedVirtualTables['statehistory'] = true; + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->requireVirtualTable('services'); + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->requireVirtualTable('services'); + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = sh.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join services + */ + protected function joinServices() + { + $this->select->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id', + array() + ); + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('services'); + + return ['so.object_id', 'so.object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php new file mode 100644 index 0000000..fafa03b --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php @@ -0,0 +1,524 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for service status + */ +class ServicestatusQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $allowCustomVars = true; + + /** + * {@inheritdoc} + */ + protected $groupBase = array('services' => array('so.object_id', 's.service_id')); + + /** + * {@inheritdoc} + */ + protected $groupOrigin = array('hostgroups', 'servicegroups', 'contacts', 'contactgroups'); + + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup' + ); + + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'checktimeperiods' => array( + 'service_check_timeperiod' => 'ctp.alias COLLATE latin1_general_ci' + ), + 'contacts' => [ + 'service_contact' => 'sco.name1' + ], + 'contactgroups' => [ + 'service_contactgroup' => 'scgo.name1' + ], + 'hostcontacts' => [ + 'host_contact' => 'hco.name1' + ], + 'hostcontactgroups' => [ + 'host_contactgroup' => 'hcgo.name1' + ], + 'hostgroups' => array( + 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci', + 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci', + 'hostgroup_name' => 'hgo.name1' + ), + 'hosts' => array( + 'host_action_url' => 'h.action_url', + 'host_address' => 'h.address', + 'host_address6' => 'h.address6', + 'host_alias' => 'h.alias COLLATE latin1_general_ci', + 'host_display_name' => 'h.display_name COLLATE latin1_general_ci', + 'host_icon_image' => 'h.icon_image', + 'host_icon_image_alt' => 'h.icon_image_alt', + 'host_ipv4' => 'INET_ATON(h.address)', + 'host_notes' => 'h.notes', + 'host_notes_url' => 'h.notes_url' + ), + 'hoststatus' => array( + 'host_acknowledged' => 'hs.problem_has_been_acknowledged', + 'host_acknowledgement_type' => 'hs.acknowledgement_type', + 'host_active_checks_enabled' => 'hs.active_checks_enabled', + 'host_active_checks_enabled_changed' => 'CASE WHEN hs.active_checks_enabled = h.active_checks_enabled THEN 0 ELSE 1 END', + 'host_attempt' => 'hs.current_check_attempt || \'/\' || hs.max_check_attempts', + 'host_check_command' => 'hs.check_command', + 'host_check_execution_time' => 'hs.execution_time', + 'host_check_latency' => 'hs.latency', + 'host_check_source' => 'hs.check_source', + 'host_check_timeperiod_object_id' => 'hs.check_timeperiod_object_id', + 'host_check_type' => 'hs.check_type', + 'host_current_check_attempt' => 'hs.current_check_attempt', + 'host_current_notification_number' => 'hs.current_notification_number', + 'host_event_handler' => 'hs.event_handler', + 'host_event_handler_enabled' => 'hs.event_handler_enabled', + 'host_event_handler_enabled_changed' => 'CASE WHEN hs.event_handler_enabled = h.event_handler_enabled THEN 0 ELSE 1 END', + 'host_failure_prediction_enabled' => 'hs.failure_prediction_enabled', + 'host_flap_detection_enabled' => 'hs.flap_detection_enabled', + 'host_flap_detection_enabled_changed' => 'CASE WHEN hs.flap_detection_enabled = h.flap_detection_enabled THEN 0 ELSE 1 END', + 'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END', + 'host_hard_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE CASE WHEN hs.state_type = 1 THEN hs.current_state ELSE hs.last_hard_state END END', + 'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END', + 'host_is_flapping' => 'hs.is_flapping', + 'host_is_reachable' => 'hs.is_reachable', + 'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)', + 'host_last_hard_state' => 'hs.last_hard_state', + 'host_last_hard_state_change' => 'UNIX_TIMESTAMP(hs.last_hard_state_change)', + 'host_last_notification' => 'UNIX_TIMESTAMP(hs.last_notification)', + 'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)', + 'host_last_time_down' => 'UNIX_TIMESTAMP(hs.last_time_down)', + 'host_last_time_unreachable' => 'UNIX_TIMESTAMP(hs.last_time_unreachable)', + 'host_last_time_up' => 'UNIX_TIMESTAMP(hs.last_time_up)', + 'host_long_output' => 'hs.long_output', + 'host_max_check_attempts' => 'hs.max_check_attempts', + 'host_modified_host_attributes' => 'hs.modified_host_attributes', + 'host_next_check' => 'CASE hs.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END', + 'host_next_notification' => 'UNIX_TIMESTAMP(hs.next_notification)', + 'host_no_more_notifications' => 'hs.no_more_notifications', + 'host_normal_check_interval' => 'hs.normal_check_interval', + 'host_notifications_enabled' => 'hs.notifications_enabled', + 'host_notifications_enabled_changed' => 'CASE WHEN hs.notifications_enabled = h.notifications_enabled THEN 0 ELSE 1 END', + 'host_obsessing' => 'hs.obsess_over_host', + 'host_obsessing_changed' => 'CASE WHEN hs.obsess_over_host = h.obsess_over_host THEN 0 ELSE 1 END', + 'host_output' => 'hs.output', + 'host_passive_checks_enabled' => 'hs.passive_checks_enabled', + 'host_passive_checks_enabled_changed' => 'CASE WHEN hs.passive_checks_enabled = h.passive_checks_enabled THEN 0 ELSE 1 END', + 'host_percent_state_change' => 'hs.percent_state_change', + 'host_perfdata' => 'hs.perfdata', + 'host_problem' => 'CASE WHEN COALESCE(hs.current_state, 0) = 0 THEN 0 ELSE 1 END', + 'host_problem_has_been_acknowledged' => 'hs.problem_has_been_acknowledged', + 'host_process_performance_data' => 'hs.process_performance_data', + 'host_retry_check_interval' => 'hs.retry_check_interval', + 'host_scheduled_downtime_depth' => 'hs.scheduled_downtime_depth', + 'host_severity' => ' + CASE + WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL + THEN 16 + ELSE + CASE + WHEN hs.current_state = 0 + THEN 1 + ELSE + CASE + WHEN hs.current_state = 1 THEN 64 + WHEN hs.current_state = 2 THEN 32 + ELSE 256 + END + + + CASE + WHEN hs.problem_has_been_acknowledged = 1 THEN 2 + WHEN hs.scheduled_downtime_depth > 0 THEN 1 + ELSE 256 + END + END + END', + 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END', + 'host_state_type' => 'hs.state_type', + 'host_status_update_time' => 'hs.status_update_time', + 'host_unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END' + + ), + 'instances' => array( + 'instance_name' => 'i.instance_name' + ), + 'services' => array( + 'host' => 'so.name1 COLLATE latin1_general_ci', + 'host_name' => 'so.name1', + 'object_type' => '(\'service\')', + 'service' => 'so.name2 COLLATE latin1_general_ci', + 'service_action_url' => 's.action_url', + 'service_check_interval' => '(s.check_interval * 60)', + 'service_description' => 'so.name2', + 'service_display_name' => 's.display_name COLLATE latin1_general_ci', + 'service_host' => 'so.name1 COLLATE latin1_general_ci', + 'service_host_name' => 'so.name1', + 'service_icon_image' => 's.icon_image', + 'service_icon_image_alt' => 's.icon_image_alt', + 'service_notes_url' => 's.notes_url', + 'service_notes' => 's.notes' + ), + 'servicegroups' => array( + 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci', + 'servicegroup_name' => 'sgo.name1', + 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci' + ), + 'servicestatus' => array( + 'service_acknowledged' => 'ss.problem_has_been_acknowledged', + 'service_acknowledgement_type' => 'ss.acknowledgement_type', + 'service_active_checks_enabled' => 'ss.active_checks_enabled', + 'service_active_checks_enabled_changed' => 'CASE WHEN ss.active_checks_enabled=s.active_checks_enabled THEN 0 ELSE 1 END', + 'service_attempt' => 'ss.current_check_attempt || \'/\' || ss.max_check_attempts', + 'service_check_command' => 'ss.check_command', + 'service_check_execution_time' => 'ss.execution_time', + 'service_check_latency' => 'ss.latency', + 'service_check_source' => 'ss.check_source', + 'service_check_timeperiod_object_id' => 'ss.check_timeperiod_object_id', + 'service_check_type' => 'ss.check_type', + 'service_current_check_attempt' => 'ss.current_check_attempt', + 'service_current_notification_number' => 'ss.current_notification_number', + 'service_event_handler' => 'ss.event_handler', + 'service_event_handler_enabled' => 'ss.event_handler_enabled', + 'service_event_handler_enabled_changed' => 'CASE WHEN ss.event_handler_enabled=s.event_handler_enabled THEN 0 ELSE 1 END', + 'service_failure_prediction_enabled' => 'ss.failure_prediction_enabled', + 'service_flap_detection_enabled' => 'ss.flap_detection_enabled', + 'service_flap_detection_enabled_changed' => 'CASE WHEN ss.flap_detection_enabled=s.flap_detection_enabled THEN 0 ELSE 1 END', + 'service_handled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) > 0 THEN 1 ELSE 0 END', + 'service_hard_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE CASE WHEN ss.state_type = 1 THEN ss.current_state ELSE ss.last_hard_state END END', + 'service_in_downtime' => 'CASE WHEN (ss.scheduled_downtime_depth = 0 OR ss.scheduled_downtime_depth IS NULL) THEN 0 ELSE 1 END', + 'service_is_flapping' => 'ss.is_flapping', + 'service_is_passive_checked' => 'CASE WHEN ss.active_checks_enabled = 0 AND ss.passive_checks_enabled = 1 THEN 1 ELSE 0 END', + 'service_is_reachable' => 'ss.is_reachable', + 'service_last_check' => 'UNIX_TIMESTAMP(ss.last_check)', + 'service_last_hard_state' => 'ss.last_hard_state', + 'service_last_hard_state_change' => 'UNIX_TIMESTAMP(ss.last_hard_state_change)', + 'service_last_notification' => 'UNIX_TIMESTAMP(ss.last_notification)', + 'service_last_state_change' => 'UNIX_TIMESTAMP(ss.last_state_change)', + 'service_last_state_change_ts' => 'ss.last_state_change', + 'service_last_time_critical' => 'ss.last_time_critical', + 'service_last_time_ok' => 'ss.last_time_ok', + 'service_last_time_unknown' => 'ss.last_time_unknown', + 'service_last_time_warning' => 'ss.last_time_warning', + 'service_long_output' => 'ss.long_output', + 'service_max_check_attempts' => 'ss.max_check_attempts', + 'service_modified_service_attributes' => 'ss.modified_service_attributes', + 'service_next_check' => 'UNIX_TIMESTAMP(ss.next_check)', + 'service_next_notification' => 'UNIX_TIMESTAMP(ss.next_notification)', + 'service_next_update' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL + THEN + CASE ss.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(ss.next_check) + (ss.normal_check_interval * 60) ELSE NULL END + ELSE + UNIX_TIMESTAMP(ss.next_check) + + (CASE WHEN + COALESCE(ss.current_state, 0) > 0 AND ss.state_type = 0 + THEN + ss.retry_check_interval + ELSE + ss.normal_check_interval + END * 60) + + (CEIL(ss.execution_time + ss.latency) * 2) + END', + 'service_no_more_notifications' => 'ss.no_more_notifications', + 'service_normal_check_interval' => 'ss.normal_check_interval', + 'service_notifications_enabled' => 'ss.notifications_enabled', + 'service_notifications_enabled_changed' => 'CASE WHEN ss.notifications_enabled=s.notifications_enabled THEN 0 ELSE 1 END', + 'service_obsessing' => 'ss.obsess_over_service', + 'service_obsessing_changed' => 'CASE WHEN ss.obsess_over_service=s.obsess_over_service THEN 0 ELSE 1 END', + 'service_output' => 'ss.output', + 'service_passive_checks_enabled' => 'ss.passive_checks_enabled', + 'service_passive_checks_enabled_changed' => 'CASE WHEN ss.passive_checks_enabled=s.passive_checks_enabled THEN 0 ELSE 1 END', + 'service_percent_state_change' => 'ss.percent_state_change', + 'service_perfdata' => 'ss.perfdata', + 'service_problem' => 'CASE WHEN COALESCE(ss.current_state, 0) = 0 THEN 0 ELSE 1 END', + 'service_problem_has_been_acknowledged' => 'ss.problem_has_been_acknowledged', + 'service_process_performance_data' => 'ss.process_performance_data', + 'service_retry_check_interval' => 'ss.retry_check_interval', + 'service_scheduled_downtime_depth' => 'ss.scheduled_downtime_depth', + 'service_severity' => 'CASE WHEN ss.current_state = 0 + THEN + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL + THEN 16 + ELSE 0 + END + + + CASE WHEN ss.problem_has_been_acknowledged = 1 + THEN 2 + ELSE + CASE WHEN ss.scheduled_downtime_depth > 0 + THEN 1 + ELSE 4 + END + END + ELSE + CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 16 + WHEN ss.current_state = 1 THEN 32 + WHEN ss.current_state = 2 THEN 128 + WHEN ss.current_state = 3 THEN 64 + ELSE 256 + END + + + CASE WHEN hs.current_state > 0 + THEN 1024 + ELSE + CASE WHEN ss.problem_has_been_acknowledged = 1 + THEN 512 + ELSE + CASE WHEN ss.scheduled_downtime_depth > 0 + THEN 256 + ELSE 2048 + END + END + END + END', + 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END', + 'service_state_type' => 'ss.state_type', + 'service_status_update_time' => 'ss.status_update_time', + 'service_unhandled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) = 0 THEN 1 ELSE 0 END', + 'problems' => 'CASE WHEN COALESCE(ss.current_state, 0) = 0 THEN 0 ELSE 1 END' + ) + ); + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + if (version_compare($this->getIdoVersion(), '1.10.0', '<')) { + $this->columnMap['hoststatus']['host_check_source'] = '(NULL)'; + $this->columnMap['servicestatus']['service_check_source'] = '(NULL)'; + } + if (version_compare($this->getIdoVersion(), '1.13.0', '<')) { + $this->columnMap['hoststatus']['host_is_reachable'] = '(NULL)'; + $this->columnMap['servicestatus']['service_is_reachable'] = '(NULL)'; + } + + $this->select->from( + array('so' => $this->prefix . 'objects'), + array() + )->join( + array('s' => $this->prefix . 'services'), + 's.service_object_id = so.object_id AND so.is_active = 1 AND so.objecttype_id = 2', + array() + ); + $this->joinedVirtualTables['services'] = true; + } + + /** + * Join check time periods + */ + protected function joinChecktimeperiods() + { + $this->select->joinLeft( + array('ctp' => $this->prefix . 'timeperiods'), + 'ctp.timeperiod_object_id = s.check_timeperiod_object_id', + array() + ); + } + + /** + * Join contacts + */ + protected function joinContacts() + { + $this->select->joinLeft( + ['sc' => 'icinga_service_contacts'], + 'sc.service_id = s.service_id', + [] + )->joinLeft( + ['sco' => 'icinga_objects'], + 'sco.object_id = sc.contact_object_id AND sco.is_active = 1 AND sco.objecttype_id = 10', + [] + ); + } + + /** + * Join contact groups + */ + protected function joinContactgroups() + { + $this->select->joinLeft( + ['scg' => 'icinga_service_contactgroups'], + 'scg.service_id = s.service_id', + [] + )->joinLeft( + ['scgo' => 'icinga_objects'], + 'scgo.object_id = scg.contactgroup_object_id AND scgo.is_active = 1 AND scgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host contacts + */ + protected function joinHostcontacts() + { + $this->requireVirtualTable('hosts'); + + $this->select->joinLeft( + ['hc' => 'icinga_host_contacts'], + 'hc.host_id = h.host_id', + [] + )->joinLeft( + ['hco' => 'icinga_objects'], + 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10', + [] + ); + } + + /** + * Join host contact groups + */ + protected function joinHostcontactgroups() + { + $this->requireVirtualTable('hosts'); + + $this->select->joinLeft( + ['hcg' => 'icinga_host_contactgroups'], + 'hcg.host_id = h.host_id', + [] + )->joinLeft( + ['hcgo' => 'icinga_objects'], + 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11', + [] + ); + } + + /** + * Join host groups + */ + protected function joinHostgroups() + { + $this->select->joinLeft( + array('hgm' => $this->prefix . 'hostgroup_members'), + 'hgm.host_object_id = s.host_object_id', + array() + )->joinLeft( + array('hg' => $this->prefix . 'hostgroups'), + 'hg.hostgroup_id = hgm.hostgroup_id', + array() + )->joinLeft( + array('hgo' => $this->prefix . 'objects'), + 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3', + array() + ); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $this->select->join( + array('h' => $this->prefix . 'hosts'), + 'h.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join host status + */ + protected function joinHoststatus() + { + $this->select->join( + array('hs' => $this->prefix . 'hoststatus'), + 'hs.host_object_id = s.host_object_id', + array() + ); + } + + /** + * Join instances + */ + protected function joinInstances() + { + $this->select->join( + array('i' => $this->prefix . 'instances'), + 'i.instance_id = so.instance_id', + array() + ); + } + + /** + * Join service groups + */ + protected function joinServicegroups() + { + $this->select->joinLeft( + array('sgm' => $this->prefix . 'servicegroup_members'), + 'sgm.service_object_id = so.object_id', + array() + )->joinLeft( + array('sg' => $this->prefix . 'servicegroups'), + 'sg.servicegroup_id = sgm.servicegroup_id', + array() + )->joinLeft( + array('sgo' => $this->prefix . 'objects'), + 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4', + array() + ); + } + + /** + * Join service status + */ + protected function joinServicestatus() + { + $this->requireVirtualTable('hoststatus'); + $this->select->join( + array('ss' => $this->prefix . 'servicestatus'), + 'ss.service_object_id = so.object_id', + array() + ); + } + + /** + * {@inheritdoc} + */ + protected function registerGroupColumns($alias, $table, array &$groupedColumns, array &$groupedTables) + { + if ($alias === 'service_handled' || $alias === 'service_severity' || $alias === 'service_unhandled') { + if (! isset($groupedTables['hoststatus'])) { + $groupedColumns[] = 'hs.hoststatus_id'; + $groupedTables['hoststatus'] = true; + } + + if (! isset($groupedTables['servicestatus'])) { + $groupedColumns[] = 'ss.servicestatus_id'; + $groupedTables['servicestatus'] = true; + } + } elseif ($table === 'contacts') { + $groupedColumns[] = 'sc.service_contact_id'; + $groupedColumns[] = 'sco.object_id'; + $groupedTables[$table] = true; + } elseif ($table === 'contactgroups') { + $groupedColumns[] = 'scg.service_contactgroup_id'; + $groupedColumns[] = 'scgo.object_id'; + $groupedTables[$table] = true; + } else { + parent::registerGroupColumns($alias, $table, $groupedColumns, $groupedTables); + } + } + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'hostgroup') { + $query->joinVirtualTable('members'); + + return ['hgm.host_object_id', 's.host_object_id']; + } elseif ($name === 'servicegroup') { + $query->joinVirtualTable('members'); + + return ['sgm.service_object_id', 'so.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php new file mode 100644 index 0000000..cf59cf3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php @@ -0,0 +1,104 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; + +/** + * Query for service status summary + * + * TODO(el): Allow to switch between hard and soft states + */ +class ServicestatussummaryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'servicestatussummary' => array( + 'services_critical' => 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END)', + 'services_critical_handled' => 'SUM(CASE WHEN state = 2 AND handled = 1 THEN 1 ELSE 0 END)', +// 'services_critical_handled_last_state_change' => 'MAX(CASE WHEN state = 2 AND handled = 1 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_critical_unhandled' => 'SUM(CASE WHEN state = 2 AND handled = 0 THEN 1 ELSE 0 END)', +// 'services_critical_unhandled_last_state_change' => 'MAX(CASE WHEN state = 2 AND handled = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_ok' => 'SUM(CASE WHEN state = 0 THEN 1 ELSE 0 END)', +// 'services_ok_last_state_change' => 'MAX(CASE WHEN state = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_pending' => 'SUM(CASE WHEN state = 99 THEN 1 ELSE 0 END)', +// 'services_pending_last_state_change' => 'MAX(CASE WHEN state = 99 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_total' => 'SUM(1)', + 'services_unknown' => 'SUM(CASE WHEN state = 3 THEN 1 ELSE 0 END)', + 'services_unknown_handled' => 'SUM(CASE WHEN state = 3 AND handled = 1 THEN 1 ELSE 0 END)', +// 'services_unknown_handled_last_state_change' => 'MAX(CASE WHEN state = 3 AND handled = 1 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_unknown_unhandled' => 'SUM(CASE WHEN state = 3 AND handled = 0 THEN 1 ELSE 0 END)', +// 'services_unknown_unhandled_last_state_change' => 'MAX(CASE WHEN state = 3 AND handled = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_warning' => 'SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END)', + 'services_warning_handled' => 'SUM(CASE WHEN state = 1 AND handled = 1 THEN 1 ELSE 0 END)', +// 'services_warning_handled_last_state_change' => 'MAX(CASE WHEN state = 1 AND handled = 1 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)', + 'services_warning_unhandled' => 'SUM(CASE WHEN state = 1 AND handled = 0 THEN 1 ELSE 0 END)', +// 'services_warning_unhandled_last_state_change' => 'MAX(CASE WHEN state = 1 AND handled = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)' + ) + ); + + /** + * The service status sub select + * + * @var ServiceStatusQuery + */ + protected $subSelect; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + return $this->subSelect->allowsCustomVars(); + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + $this->subSelect->applyFilter(clone $filter); + return $this; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + // TODO(el): Allow to switch between hard and soft states + $this->subSelect = $this->createSubQuery( + 'servicestatus', + array( + 'handled' => 'service_handled', + 'state' => 'service_state', + 'state_change' => 'service_last_state_change' + ) + ); + $this->select->from( + array('servicestatussummary' => $this->subSelect->setIsSubQuery(true)), + array() + ); + $this->joinedVirtualTables['servicestatussummary'] = true; + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->subSelect->where($condition, $value); + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->subSelect->whereEx($ex); + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php new file mode 100644 index 0000000..18d893f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php @@ -0,0 +1,41 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +/** + * Query for host and service state change events + */ +class StatechangeeventQuery extends IdoQuery +{ + protected $columnMap = array( + 'statechangeevent' => array( + 'statechangeevent_id' => 'sh.statehistory_id', + 'statechangeevent_state_time' => 'UNIX_TIMESTAMP(sh.state_time)', + 'statechangeevent_state_change' => 'sh.state_change', + 'statechangeevent_state' => 'sh.state', + 'statechangeevent_state_type' => "(CASE sh.state_type WHEN 0 THEN 'soft_state' WHEN 1 THEN 'hard_state' ELSE NULL END)", + 'statechangeevent_current_check_attempt' => 'sh.current_check_attempt', + 'statechangeevent_max_check_attempts' => 'sh.max_check_attempts', + 'statechangeevent_last_state' => 'sh.last_state', + 'statechangeevent_last_hard_state' => 'sh.last_hard_state', + 'statechangeevent_output' => 'sh.output', + 'statechangeevent_long_output' => 'sh.long_output', + 'statechangeevent_check_source' => 'sh.check_source' + ), + 'object' => array( + 'host_name' => 'o.name1', + 'service_description' => 'o.name2' + ) + ); + + protected function joinBaseTables() + { + $this->select() + ->from(array('sh' => $this->prefix . 'statehistory'), array()) + ->join(array('o' => $this->prefix . 'objects'), 'sh.object_id = o.object_id', array()); + + $this->joinedVirtualTables['statechangeevent'] = true; + $this->joinedVirtualTables['object'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php new file mode 100644 index 0000000..56d1e3b --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service state history records + */ +class StatehistoryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'statehistory' => array( + 'id' => 'sth.id', + 'object_type' => 'sth.object_type' + ), + 'history' => array( + 'type' => 'sth.type', + 'timestamp' => 'sth.timestamp', + 'object_id' => 'sth.object_id', + 'state' => 'sth.state', + 'output' => 'sth.output' + ), + 'hosts' => array( + 'host_display_name' => 'sth.host_display_name', + 'host_name' => 'sth.host_name' + ), + 'services' => array( + 'service_description' => 'sth.service_description', + 'service_display_name' => 'sth.service_display_name', + 'service_host_name' => 'sth.service_host_name' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $stateHistoryQuery; + + /** + * Subqueries used for the state history query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * Whether to additionally select all history columns + * + * @var bool + */ + protected $fetchHistoryColumns = false; + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + $this->stateHistoryQuery = $this->db->select(); + $this->select->from( + array('sth' => $this->stateHistoryQuery), + array() + ); + $this->joinedVirtualTables['statehistory'] = true; + } + + /** + * Join history related columns and tables + */ + protected function joinHistory() + { + // TODO: Ensure that one is selecting the history columns first... + $this->fetchHistoryColumns = true; + $this->requireVirtualTable('hosts'); + $this->requireVirtualTable('services'); + } + + /** + * Join hosts + */ + protected function joinHosts() + { + $columns = array_keys( + $this->columnMap['statehistory'] + $this->columnMap['hosts'] + ); + foreach ($this->columnMap['services'] as $column => $_) { + $columns[$column] = new Zend_Db_Expr('NULL'); + } + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $hosts = $this->createSubQuery('Hoststatehistory', $columns); + $this->subQueries[] = $hosts; + $this->stateHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * Join services + */ + protected function joinServices() + { + $columns = array_keys( + $this->columnMap['statehistory'] + $this->columnMap['hosts'] + $this->columnMap['services'] + ); + if ($this->fetchHistoryColumns) { + $columns = array_merge($columns, array_keys($this->columnMap['history'])); + } + $services = $this->createSubQuery('Servicestatehistory', $columns); + $this->subQueries[] = $services; + $this->stateHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL); + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php new file mode 100644 index 0000000..b1ee9e2 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php @@ -0,0 +1,243 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\FilterExpression; +use Zend_Db_Expr; +use Zend_Db_Select; +use Icinga\Data\Filter\Filter; + +/** + * Query for host and service status summary + */ +class StatussummaryQuery extends IdoQuery +{ + /** + * {@inheritdoc} + */ + protected $columnMap = array( + 'hoststatussummary' => array( + 'hosts_total' => 'SUM(CASE WHEN object_type = \'host\' THEN 1 ELSE 0 END)', + 'hosts_up' => 'SUM(CASE WHEN object_type = \'host\' AND state = 0 THEN 1 ELSE 0 END)', + 'hosts_up_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'hosts_pending' => 'SUM(CASE WHEN object_type = \'host\' AND state = 99 THEN 1 ELSE 0 END)', + 'hosts_pending_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'hosts_down' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 THEN 1 ELSE 0 END)', + 'hosts_down_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND handled > 0 THEN 1 ELSE 0 END)', + 'hosts_down_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND handled = 0 THEN 1 ELSE 0 END)', + 'hosts_down_passive' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'hosts_down_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'hosts_unreachable' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 THEN 1 ELSE 0 END)', + 'hosts_unreachable_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND handled > 0 THEN 1 ELSE 0 END)', + 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND handled = 0 THEN 1 ELSE 0 END)', + 'hosts_unreachable_passive' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'hosts_unreachable_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'hosts_active' => 'SUM(CASE WHEN object_type = \'host\' AND is_active_checked = 1 THEN 1 ELSE 0 END)', + 'hosts_passive' => 'SUM(CASE WHEN object_type = \'host\' AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'hosts_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'hosts_not_processing_event_handlers' => 'SUM(CASE WHEN object_type = \'host\' AND is_processing_events = 0 THEN 1 ELSE 0 END)', + 'hosts_not_triggering_notifications' => 'SUM(CASE WHEN object_type = \'host\' AND is_triggering_notifications = 0 THEN 1 ELSE 0 END)', + 'hosts_without_flap_detection' => 'SUM(CASE WHEN object_type = \'host\' AND is_allowed_to_flap = 0 THEN 1 ELSE 0 END)', + 'hosts_flapping' => 'SUM(CASE WHEN object_type = \'host\' AND is_flapping = 1 THEN 1 ELSE 0 END)' + ), + 'servicestatussummary' => array( + 'services_total' => 'SUM(CASE WHEN object_type = \'service\' THEN 1 ELSE 0 END)', + 'services_problem' => 'SUM(CASE WHEN object_type = \'service\' AND state > 0 THEN 1 ELSE 0 END)', + 'services_problem_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state > 0 AND handled + host_problem > 0 THEN 1 ELSE 0 END)', + 'services_problem_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state > 0 AND handled + host_problem = 0 THEN 1 ELSE 0 END)', + 'services_ok' => 'SUM(CASE WHEN object_type = \'service\' AND state = 0 THEN 1 ELSE 0 END)', + 'services_ok_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_pending' => 'SUM(CASE WHEN object_type = \'service\' AND state = 99 THEN 1 ELSE 0 END)', + 'services_pending_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_warning' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 THEN 1 ELSE 0 END)', + 'services_warning_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND handled + host_problem > 0 THEN 1 ELSE 0 END)', + 'services_warning_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND handled + host_problem = 0 THEN 1 ELSE 0 END)', + 'services_warning_passive' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_warning_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_critical' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 THEN 1 ELSE 0 END)', + 'services_critical_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND handled + host_problem > 0 THEN 1 ELSE 0 END)', + 'services_critical_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND handled + host_problem = 0 THEN 1 ELSE 0 END)', + 'services_critical_passive' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_critical_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_unknown' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 THEN 1 ELSE 0 END)', + 'services_unknown_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND handled + host_problem > 0 THEN 1 ELSE 0 END)', + 'services_unknown_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND handled + host_problem = 0 THEN 1 ELSE 0 END)', + 'services_unknown_passive' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_unknown_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_active' => 'SUM(CASE WHEN object_type = \'service\' AND is_active_checked = 1 THEN 1 ELSE 0 END)', + 'services_passive' => 'SUM(CASE WHEN object_type = \'service\' AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_not_processing_event_handlers' => 'SUM(CASE WHEN object_type = \'service\' AND is_processing_events = 0 THEN 1 ELSE 0 END)', + 'services_not_triggering_notifications' => 'SUM(CASE WHEN object_type = \'service\' AND is_triggering_notifications = 0 THEN 1 ELSE 0 END)', + 'services_without_flap_detection' => 'SUM(CASE WHEN object_type = \'service\' AND is_allowed_to_flap = 0 THEN 1 ELSE 0 END)', + 'services_flapping' => 'SUM(CASE WHEN object_type = \'service\' AND is_flapping = 1 THEN 1 ELSE 0 END)', + +/* +NOTE: in case you might wonder, please see #7303. As a quickfix I did: + +:%s/(host_state = 0 OR host_state = 99)/host_state != 1 AND host_state != 2/g +:%s/(host_state = 1 OR host_state = 2)/host_state != 0 AND host_state != 99/g + +We have to find a better solution here. + +*/ + 'services_ok_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 0 THEN 1 ELSE 0 END)', + 'services_ok_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_pending_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 99 THEN 1 ELSE 0 END)', + 'services_pending_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_warning_handled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND handled > 0 THEN 1 ELSE 0 END)', + 'services_warning_unhandled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND handled = 0 THEN 1 ELSE 0 END)', + 'services_warning_passive_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_warning_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_critical_handled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND handled > 0 THEN 1 ELSE 0 END)', + 'services_critical_unhandled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND handled = 0 THEN 1 ELSE 0 END)', + 'services_critical_passive_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_critical_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_unknown_handled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND handled > 0 THEN 1 ELSE 0 END)', + 'services_unknown_unhandled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND handled = 0 THEN 1 ELSE 0 END)', + 'services_unknown_passive_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_unknown_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_ok_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 0 THEN 1 ELSE 0 END)', + 'services_ok_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_pending_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 99 THEN 1 ELSE 0 END)', + 'services_pending_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_warning_handled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND handled > 0 THEN 1 ELSE 0 END)', + 'services_warning_unhandled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND handled = 0 THEN 1 ELSE 0 END)', + 'services_warning_passive_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_warning_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_critical_handled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND handled > 0 THEN 1 ELSE 0 END)', + 'services_critical_unhandled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND handled = 0 THEN 1 ELSE 0 END)', + 'services_critical_passive_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_critical_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)', + 'services_unknown_handled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND handled > 0 THEN 1 ELSE 0 END)', + 'services_unknown_unhandled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND handled = 0 THEN 1 ELSE 0 END)', + 'services_unknown_passive_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND is_passive_checked = 1 THEN 1 ELSE 0 END)', + 'services_unknown_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)' + ) + ); + + /** + * The union + * + * @var Zend_Db_Select + */ + protected $summaryQuery; + + /** + * Subqueries used for the summary query + * + * @var IdoQuery[] + */ + protected $subQueries = array(); + + /** + * {@inheritdoc} + */ + public function allowsCustomVars() + { + foreach ($this->subQueries as $query) { + if (! $query->allowsCustomVars()) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + foreach ($this->subQueries as $sub) { + $sub->applyFilter(clone $filter); + } + return $this; + } + + /** + * {@inheritdoc} + */ + protected function joinBaseTables() + { + // TODO(el): Allow to switch between hard and soft states + $hosts = $this->createSubQuery( + 'Hoststatus', + array( + 'handled' => 'host_handled', + 'host_problem', + 'host_state' => new Zend_Db_Expr('NULL'), + 'is_active_checked' => 'host_active_checks_enabled', + 'is_allowed_to_flap' => 'host_flap_detection_enabled', + 'is_flapping' => 'host_is_flapping', + 'is_passive_checked' => 'host_is_passive_checked', + 'is_processing_events' => 'host_event_handler_enabled', + 'is_triggering_notifications' => 'host_notifications_enabled', + 'object_type', + 'severity' => 'host_severity', + 'state_change' => 'host_last_state_change', + 'state' => 'host_state' + ) + ); + $this->subQueries[] = $hosts; + $services = $this->createSubQuery( + 'Servicestatus', + array( + 'handled' => 'service_handled', + 'host_problem', + 'host_state' => 'host_hard_state', + 'is_active_checked' => 'service_active_checks_enabled', + 'is_allowed_to_flap' => 'service_flap_detection_enabled', + 'is_flapping' => 'service_is_flapping', + 'is_passive_checked' => 'service_is_passive_checked', + 'is_processing_events' => 'service_event_handler_enabled', + 'is_triggering_notifications' => 'service_notifications_enabled', + 'object_type', + 'severity' => 'service_severity', + 'state_change' => 'service_last_state_change', + 'state' => 'service_state' + ) + ); + $this->subQueries[] = $services; + $this->summaryQuery = $this->db->select()->union(array($hosts, $services), Zend_Db_Select::SQL_UNION_ALL); + $this->select->from(array('statussummary' => $this->summaryQuery), array()); + $this->joinedVirtualTables['hoststatussummary'] = true; + $this->joinedVirtualTables['servicestatussummary'] = true; + } + + /** + * {@inheritdoc} + */ + public function order($columnOrAlias, $dir = null) + { + if (! $this->hasAliasName($columnOrAlias)) { + foreach ($this->subQueries as $sub) { + $sub->requireColumn($columnOrAlias); + } + } + return parent::order($columnOrAlias, $dir); + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + $this->requireColumn($condition); + foreach ($this->subQueries as $sub) { + $sub->where($condition, $value); + } + return $this; + } + + public function whereEx(FilterExpression $ex) + { + $this->requireColumn($ex->getColumn()); + foreach ($this->subQueries as $sub) { + $sub->whereEx($ex); + } + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php new file mode 100644 index 0000000..f4c4e07 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; + +/** + * Query for unhandled host problems + */ +class UnhandledhostproblemsQuery extends IdoQuery +{ + protected $allowCustomVars = true; + + protected $columnMap = array( + 'problems' => array( + 'hosts_down_unhandled' => 'COUNT(*)', + ) + ); + + /** + * The service status sub select + * + * @var HoststatusQuery + */ + protected $subSelect; + + public function addFilter(Filter $filter) + { + $this->subSelect->applyFilter(clone $filter); + return $this; + } + + protected function joinBaseTables() + { + $this->subSelect = $this->createSubQuery( + 'Hoststatus', + array('host_name') + ); + $this->subSelect->where('host_handled', 0); + $this->subSelect->where('host_state', 1); + $this->select->from( + array('problems' => $this->subSelect->setIsSubQuery(true)), + array() + ); + $this->joinedVirtualTables['problems'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php new file mode 100644 index 0000000..a218caf --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend\Ido\Query; + +use Icinga\Data\Filter\Filter; + +/** + * Query for unhandled service problems + */ +class UnhandledserviceproblemsQuery extends IdoQuery +{ + protected $allowCustomVars = true; + + protected $columnMap = array( + 'problems' => array( + 'services_critical_unhandled' => 'COUNT(*)', + ) + ); + + /** + * The service status sub select + * + * @var ServicestatusQuery + */ + protected $subSelect; + + public function addFilter(Filter $filter) + { + $this->subSelect->applyFilter(clone $filter); + return $this; + } + + protected function joinBaseTables() + { + $this->subSelect = $this->createSubQuery( + 'Servicestatus', + array('service_description') + ); + $this->subSelect->where('service_handled', 0); + $this->subSelect->where('service_state', 2); + $this->select->from( + array('problems' => $this->subSelect->setIsSubQuery(true)), + array() + ); + $this->joinedVirtualTables['problems'] = true; + } +} diff --git a/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php new file mode 100644 index 0000000..5400957 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php @@ -0,0 +1,348 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Backend; + +use Icinga\Application\Config; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Data\ConnectionInterface; +use Icinga\Data\Queryable; +use Icinga\Data\Selectable; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\ProgrammingError; + +class MonitoringBackend implements Selectable, Queryable, ConnectionInterface +{ + /** + * Backend configuration + * + * @var ConfigObject + */ + protected $config; + + /** + * Resource + * + * @var mixed + */ + protected $resource; + + /** + * Type + * + * @var string + */ + protected $type; + + /** + * The configured name of this backend + * + * @var string + */ + protected $name; + + /** + * Already created instances + * + * @var array + */ + protected static $instances = array(); + + /** + * Create a new backend + * + * @param string $name + * @param ConfigObject $config + */ + protected function __construct($name, ConfigObject $config) + { + $this->name = $name; + $this->config = $config; + } + + /** + * Get a backend instance + * + * You may ask for a specific backend name or get the default one otherwise + * + * @param string $name Backend name + * + * @return MonitoringBackend + */ + public static function instance($name = null) + { + if (! array_key_exists($name, self::$instances)) { + list($foundName, $config) = static::loadConfig($name); + $type = $config->get('type'); + $class = implode( + '\\', + array( + __NAMESPACE__, + ucfirst($type), + ucfirst($type) . 'Backend' + ) + ); + + if (!class_exists($class)) { + throw new ConfigurationError( + mt('monitoring', 'There is no "%s" monitoring backend'), + $class + ); + } + + self::$instances[$name] = new $class($foundName, $config); + if ($name === null) { + self::$instances[$foundName] = self::$instances[$name]; + } + } + + return self::$instances[$name]; + } + + /** + * Clear all cached instances. Mostly for testing purposes. + */ + public static function clearInstances() + { + self::$instances = array(); + } + + /** + * Whether this backend is of a specific type + * + * @param string $type Backend type + * + * @return boolean + */ + public function is($type) + { + return $this->getType() === $type; + } + + /** + * Get the configured name of this backend + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the backend type name + * + * @return string + */ + public function getType() + { + if ($this->type === null) { + $parts = preg_split('~\\\~', get_class($this)); + $class = array_pop($parts); + if (substr($class, -7) === 'Backend') { + $this->type = lcfirst(substr($class, 0, -7)); + } else { + throw new ProgrammingError( + '%s is not a valid monitoring backend class name', + $class + ); + } + } + return $this->type; + } + + /** + * Return the configuration for the first enabled or the given backend + */ + protected static function loadConfig($name = null) + { + $backends = Config::module('monitoring', 'backends'); + + if ($name === null) { + $count = 0; + + foreach ($backends as $name => $config) { + $count++; + if ((bool) $config->get('disabled', false) === false) { + return array($name, $config); + } + } + + if ($count === 0) { + $message = mt('monitoring', 'No backend has been configured'); + } else { + $message = mt('monitoring', 'All backends are disabled'); + } + + throw new ConfigurationError($message); + } else { + $config = $backends->getSection($name); + + if ($config->isEmpty()) { + throw new ConfigurationError( + mt('monitoring', 'No configuration for backend %s'), + $name + ); + } + + if ((bool) $config->get('disabled', false) === true) { + throw new ConfigurationError( + mt('monitoring', 'Configuration for backend %s is disabled'), + $name + ); + } + + return array($name, $config); + } + } + + /** + * Get this backend's internal resource + * + * @return mixed + */ + public function getResource() + { + if ($this->resource === null) { + $config = ResourceFactory::getResourceConfig($this->config->get('resource')); + if ($this->is('ido') && $config->type === 'db' && $config->db === 'mysql' && $config->charset === null) { + $config->charset = 'latin1'; + } + $this->resource = ResourceFactory::createResource($config); + if ($this->is('ido') && $this->resource->getDbType() !== 'oracle') { + // TODO(el): The resource should set the table prefix + $this->resource->setTablePrefix('icinga_'); + } + } + return $this->resource; + } + + /** + * Backend entry point + * + * @return $this + */ + public function select() + { + return $this; + } + + /** + * Create a data view to fetch data from + * + * @param string $name + * @param array $columns + * + * @return \Icinga\Module\Monitoring\DataView\DataView + */ + public function from($name, array $columns = null) + { + $class = $this->buildViewClassName($name); + return new $class($this, $columns); + } + + /** + * View name to class name resolution + * + * @param string $view + * + * @return string + * + * @throws ProgrammingError In case the view does not exist + */ + protected function buildViewClassName($view) + { + $class = ucfirst(strtolower($view)); + $classPath = '\\Icinga\\Module\\Monitoring\\DataView\\' . $class; + if (! class_exists($classPath)) { + throw new ProgrammingError('DataView %s does not exist', $class); + } + + return $classPath; + } + + /** + * Get a specific query class instance + * + * @param string $name Query name + * @param array $columns Optional column list + * + * @return Icinga\Data\QueryInterface + * + * @throws ProgrammingError When the query does not exist for this backend + */ + public function query($name, $columns = null) + { + $class = $this->buildQueryClassName($name); + + if (!class_exists($class)) { + throw new ProgrammingError( + 'Query "%s" does not exist for backend %s', + $name, + $this->getType() + ); + } + + return new $class($this->getResource(), $columns); + } + + /** + * Whether this backend supports the given query + * + * @param string $name Query name to check for + * + * @return bool + */ + public function hasQuery($name) + { + return class_exists($this->buildQueryClassName($name)); + } + + /** + * Query name to class name resolution + * + * @param string $query + * + * @return string + */ + protected function buildQueryClassName($query) + { + $parts = preg_split('~\\\~', get_class($this)); + array_pop($parts); + array_push($parts, 'Query', ucfirst(strtolower($query)) . 'Query'); + return implode('\\', $parts); + } + + /** + * Fetch and return the program version of the current instance + * + * @return string + */ + public function getProgramVersion() + { + return preg_replace( + '/^[vr]/', + '', + $this->select()->from('programstatus', array('program_version'))->fetchOne() + ); + } + + /** + * Get whether the backend is Icinga 2 + * + * @param string $programVersion + * + * @return bool + */ + public function isIcinga2($programVersion = null) + { + if ($programVersion === null) { + $programVersion = $this->select()->from('programstatus', array('program_version'))->fetchOne(); + } + return (bool) preg_match( + '/^[vr]?2\.\d+\.\d+.*$/', + $programVersion + ); + } +} diff --git a/modules/monitoring/library/Monitoring/BackendStep.php b/modules/monitoring/library/Monitoring/BackendStep.php new file mode 100644 index 0000000..e94625f --- /dev/null +++ b/modules/monitoring/library/Monitoring/BackendStep.php @@ -0,0 +1,206 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring; + +use Exception; +use Icinga\Module\Setup\Step; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; + +class BackendStep extends Step +{ + protected $data; + + protected $backendIniError; + + protected $resourcesIniError; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $success = $this->createBackendsIni(); + $success &= $this->createResourcesIni(); + return $success; + } + + protected function createBackendsIni() + { + $config = array(); + $config[$this->data['backendConfig']['name']] = array( + 'type' => $this->data['backendConfig']['type'], + 'resource' => $this->data['resourceConfig']['name'] + ); + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('modules/monitoring/backends.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->backendIniError = $e; + return false; + } + + $this->backendIniError = false; + return true; + } + + protected function createResourcesIni() + { + $resourceConfig = $this->data['resourceConfig']; + $resourceName = $resourceConfig['name']; + unset($resourceConfig['name']); + + try { + $config = Config::app('resources', true); + $config->setSection($resourceName, $resourceConfig); + $config->saveIni(); + } catch (Exception $e) { + $this->resourcesIniError = $e; + return false; + } + + $this->resourcesIniError = false; + return true; + } + + public function getSummary() + { + $pageTitle = '<h2>' . mt('monitoring', 'Monitoring Backend', 'setup.page.title') . '</h2>'; + $backendDescription = '<p>' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will retrieve information from your monitoring environment' + . ' using a backend called "%s" and the specified resource below:' + ), + $this->data['backendConfig']['name'] + ) . '</p>'; + + if ($this->data['resourceConfig']['type'] === 'db') { + $resourceTitle = '<h3>' . mt('monitoring', 'Database Resource') . '</h3>'; + $resourceHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Resource Name') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Database Type') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['db'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Host') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['host'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Port') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Database Name') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['dbname'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Username') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['username'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Password') . '</strong></td>' + . '<td>' . str_repeat('*', strlen($this->data['resourceConfig']['password'])) . '</td>' + . '</tr>'; + + if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && isset($this->data['resourceConfig']['ssl_do_not_verify_server_cert']) + && $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] + ) { + $resourceHtml .= '' + . '<tr>' + . '<td><strong>' . t('SSL Do Not Verify Server Certificate') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] . '</td>' + . '</tr>'; + } + if (isset($this->data['resourceConfig']['ssl_key']) && $this->data['resourceConfig']['ssl_key']) { + $resourceHtml .= '' + .'<tr>' + . '<td><strong>' . t('SSL Key') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_key'] . '</td>' + . '</tr>'; + } + if (isset($this->data['resourceConfig']['ssl_cert']) && $this->data['resourceConfig']['ssl_cert']) { + $resourceHtml .= '' + . '<tr>' + . '<td><strong>' . t('SSL Cert') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_cert'] . '</td>' + . '</tr>'; + } + if (isset($this->data['resourceConfig']['ssl_ca']) && $this->data['resourceConfig']['ssl_ca']) { + $resourceHtml .= '' + . '<tr>' + . '<td><strong>' . t('CA') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_ca'] . '</td>' + . '</tr>'; + } + if (isset($this->data['resourceConfig']['ssl_capath']) && $this->data['resourceConfig']['ssl_capath']) { + $resourceHtml .= '' + . '<tr>' + . '<td><strong>' . t('CA Path') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_capath'] . '</td>' + . '</tr>'; + } + if (isset($this->data['resourceConfig']['ssl_cipher']) && $this->data['resourceConfig']['ssl_cipher']) { + $resourceHtml .= '' + . '<tr>' + . '<td><strong>' . t('Cipher') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_cipher'] . '</td>' + . '</tr>'; + } + + $resourceHtml .= '' + . '</tbody>' + . '</table>'; + } + + return $pageTitle . '<div class="topic">' . $backendDescription . $resourceTitle . $resourceHtml . '</div>'; + } + + public function getReport() + { + $report = array(); + + if ($this->backendIniError === false) { + $report[] = sprintf( + mt('monitoring', 'Monitoring backend configuration has been successfully written to: %s'), + Config::resolvePath('modules/monitoring/backends.ini') + ); + } elseif ($this->backendIniError !== null) { + $report[] = sprintf( + mt( + 'monitoring', + 'Monitoring backend configuration could not be written to: %s. An error occured:' + ), + Config::resolvePath('modules/monitoring/backends.ini') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->backendIniError)); + } + + if ($this->resourcesIniError === false) { + $report[] = sprintf( + mt('monitoring', 'Resource configuration has been successfully updated: %s'), + Config::resolvePath('resources.ini') + ); + } elseif ($this->resourcesIniError !== null) { + $report[] = sprintf( + mt('monitoring', 'Resource configuration could not be udpated: %s. An error occured:'), + Config::resolvePath('resources.ini') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->resourcesIniError)); + } + + return $report; + } +} diff --git a/modules/monitoring/library/Monitoring/Cli/CliUtils.php b/modules/monitoring/library/Monitoring/Cli/CliUtils.php new file mode 100644 index 0000000..3d7d3ee --- /dev/null +++ b/modules/monitoring/library/Monitoring/Cli/CliUtils.php @@ -0,0 +1,122 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Cli; + +use Icinga\Cli\Screen; + +class CliUtils +{ + protected $hostColors = array( + 0 => array('black', 'lightgreen'), + 1 => array('black', 'lightred'), + 2 => array('black', 'brown'), + 99 => array('black', 'lightgray'), + ); + protected $serviceColors = array( + 0 => array('black', 'lightgreen'), + 1 => array('black', 'yellow'), + 2 => array('black', 'lightred'), + 3 => array('black', 'lightpurple'), + 99 => array('black', 'lightgray'), + ); + protected $hostStates = array( + 0 => 'UP', + 1 => 'DOWN', + 2 => 'UNREACHABLE', + 99 => 'PENDING', + ); + + protected $serviceStates = array( + 0 => 'OK', + 1 => 'WARNING', + 2 => 'CRITICAL', + 3 => 'UNKNOWN', + 99 => 'PENDING', + ); + + protected $screen; + protected $hostState; + protected $serviceState; + + public function __construct(Screen $screen) + { + $this->screen = $screen; + } + + public function setHostState($state) + { + $this->hostState = $state; + } + + public function setServiceState($state) + { + $this->serviceState = $state; + } + + public function shortHostState($state = null) + { + if ($state === null) { + $state = $this->hostState; + } + return sprintf('%-4s', substr($this->hostStates[$state], 0, 4)); + } + + public function shortServiceState($state = null) + { + if ($state === null) { + $state = $this->serviceState; + } + return sprintf('%-4s', substr($this->serviceStates[$state], 0, 4)); + } + + public function hostStateBackground($text, $state = null) + { + if ($state === null) { + $state = $this->hostState; + } + return $this->screen->colorize( + $text, + $this->hostColors[$state][0], + $this->hostColors[$state][1] + ); + } + + public function serviceStateBackground($text, $state = null) + { + if ($state === null) { + $state = $this->serviceState; + } + return $this->screen->colorize( + $text, + $this->serviceColors[$state][0], + $this->serviceColors[$state][1] + ); + } + + public function objectStateFlags($type, &$row) + { + $extra = array(); + if ($row->{$type . '_in_downtime'}) { + if ($this->screen->hasUtf8()) { + $extra[] = 'DOWNTIME ⌚'; + } else { + $extra[] = 'DOWNTIME'; + } + } + if ($row->{$type . '_acknowledged'}) { + if ($this->screen->hasUtf8()) { + $extra[] = 'ACK ✓'; + } else { + $extra[] = 'ACK'; + } + } + + if (empty($extra)) { + $extra = ''; + } else { + $extra = sprintf(' [ %s ]', implode(', ', $extra)); + } + return $extra; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php b/modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php new file mode 100644 index 0000000..c33157f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php @@ -0,0 +1,126 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command; + +class IcingaApiCommand +{ + /** + * Command data + * + * @var array + */ + protected $data; + + /** + * Name of the endpoint + * + * @var string + */ + protected $endpoint; + + /** + * Next Icinga API command to be sent, if any + * + * @var static + */ + protected $next; + + /** + * Create a new Icinga 2 API command + * + * @param string $endpoint + * @param array $data + * + * @return static + */ + public static function create($endpoint, array $data) + { + $command = new static(); + $command + ->setEndpoint($endpoint) + ->setData($data); + return $command; + } + + /** + * Get the command data + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Set the command data + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } + + /** + * Get the name of the endpoint + * + * @return string + */ + public function getEndpoint() + { + return $this->endpoint; + } + + /** + * Set the name of the endpoint + * + * @param string $endpoint + * + * @return $this + */ + public function setEndpoint($endpoint) + { + $this->endpoint = $endpoint; + + return $this; + } + + /** + * Get whether another Icinga API command should be sent after this one + * + * @return bool + */ + public function hasNext() + { + return $this->next !== null; + } + + /** + * Get the next Icinga API command + * + * @return IcingaApiCommand + */ + public function getNext() + { + return $this->next; + } + + /** + * Set the next Icinga API command + * + * @param IcingaApiCommand $next + * + * @return IcingaApiCommand + */ + public function setNext(IcingaApiCommand $next) + { + $this->next = $next; + return $next; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/IcingaCommand.php b/modules/monitoring/library/Monitoring/Command/IcingaCommand.php new file mode 100644 index 0000000..49ce586 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/IcingaCommand.php @@ -0,0 +1,21 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command; + +/** + * Base class for commands sent to an Icinga instance + */ +abstract class IcingaCommand +{ + /** + * Get the name of the command + * + * @return string + */ + public function getName() + { + $nsParts = explode('\\', get_called_class()); + return substr_replace(end($nsParts), '', -7); // Remove 'Command' Suffix + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php b/modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php new file mode 100644 index 0000000..1d3ce9d --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Instance; + +use Icinga\Module\Monitoring\Command\IcingaCommand; + +/** + * Disable host and service notifications w/ expire time on an Icinga instance + */ +class DisableNotificationsExpireCommand extends IcingaCommand +{ + /** + * The time when notifications should be re-enabled after disabling + * + * @var int|null Unix timestamp + */ + protected $expireTime; + + /** + * Set time when notifications should be re-enabled after disabling + * + * @param $expireTime int Unix timestamp + * + * @return $this + */ + public function setExpireTime($expireTime) + { + $this->expireTime = (int) $expireTime; + return $this; + } + + /** + * Get the date and time when notifications should be re-enabled after disabling + * + * @return int|null Unix timestamp + */ + public function getExpireTime() + { + return $this->expireTime; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php b/modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php new file mode 100644 index 0000000..8a8a8ca --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php @@ -0,0 +1,122 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Instance; + +use Icinga\Module\Monitoring\Command\IcingaCommand; + +/** + * Enable or disable a feature of an Icinga instance + */ +class ToggleInstanceFeatureCommand extends IcingaCommand +{ + /** + * Feature for enabling or disabling active host checks on an Icinga instance + */ + const FEATURE_ACTIVE_HOST_CHECKS = 'active_host_checks_enabled'; + + /** + * Feature for enabling or disabling active service checks on an Icinga instance + */ + const FEATURE_ACTIVE_SERVICE_CHECKS = 'active_service_checks_enabled'; + + /** + * Feature for enabling or disabling host and service event handlers on an Icinga instance + */ + const FEATURE_EVENT_HANDLERS = 'event_handlers_enabled'; + + /** + * Feature for enabling or disabling host and service flap detection on an Icinga instance + */ + const FEATURE_FLAP_DETECTION = 'flap_detection_enabled'; + + /** + * Feature for enabling or disabling host and service notifications on an Icinga instance + */ + const FEATURE_NOTIFICATIONS = 'notifications_enabled'; + + /** + * Feature for enabling or disabling processing of host checks via the OCHP command on an Icinga instance + */ + const FEATURE_HOST_OBSESSING = 'obsess_over_hosts'; + + /** + * Feature for enabling or disabling processing of service checks via the OCHP command on an Icinga instance + */ + const FEATURE_SERVICE_OBSESSING = 'obsess_over_services'; + + /** + * Feature for enabling or disabling passive host checks on an Icinga instance + */ + const FEATURE_PASSIVE_HOST_CHECKS = 'passive_host_checks_enabled'; + + /** + * Feature for enabling or disabling passive service checks on an Icinga instance + */ + const FEATURE_PASSIVE_SERVICE_CHECKS = 'passive_service_checks_enabled'; + + /** + * Feature for enabling or disabling the processing of host and service performance data on an Icinga instance + */ + const FEATURE_PERFORMANCE_DATA = 'process_performance_data'; + + /** + * Feature that is to be enabled or disabled + * + * @var string + */ + protected $feature; + + /** + * Whether the feature should be enabled or disabled + * + * @var bool + */ + protected $enabled; + + /** + * Set the feature that is to be enabled or disabled + * + * @param string $feature + * + * @return $this + */ + public function setFeature($feature) + { + $this->feature = (string) $feature; + return $this; + } + + /** + * Get the feature that is to be enabled or disabled + * + * @return string + */ + public function getFeature() + { + return $this->feature; + } + + /** + * Set whether the feature should be enabled or disabled + * + * @param bool $enabled + * + * @return $this + */ + public function setEnabled($enabled = true) + { + $this->enabled = (bool) $enabled; + return $this; + } + + /** + * Get whether the feature should be enabled or disabled + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php b/modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php new file mode 100644 index 0000000..2001e78 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php @@ -0,0 +1,144 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Acknowledge a host or service problem + */ +class AcknowledgeProblemCommand extends WithCommentCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST, + self::TYPE_SERVICE + ); + + /** + * Whether the acknowledgement is sticky + * + * Sticky acknowledgements remain until the host or service recovers. Non-sticky acknowledgements will be + * automatically removed when the host or service state changes. + * + * @var bool + */ + protected $sticky = false; + + /** + * Whether to send a notification about the acknowledgement + + * @var bool + */ + protected $notify = false; + + /** + * Whether the comment associated with the acknowledgement is persistent + * + * Persistent comments are not lost the next time the monitoring host restarts. + * + * @var bool + */ + protected $persistent = false; + + /** + * Optional time when the acknowledgement should expire + * + * @var int|null + */ + protected $expireTime; + + /** + * Set whether the acknowledgement is sticky + * + * @param bool $sticky + * + * @return $this + */ + public function setSticky($sticky = true) + { + $this->sticky = (bool) $sticky; + return $this; + } + + /** + * Is the acknowledgement sticky? + * + * @return bool + */ + public function getSticky() + { + return $this->sticky; + } + + /** + * Set whether to send a notification about the acknowledgement + * + * @param bool $notify + * + * @return $this + */ + public function setNotify($notify = true) + { + $this->notify = (bool) $notify; + return $this; + } + + /** + * Get whether to send a notification about the acknowledgement + * + * @return bool + */ + public function getNotify() + { + return $this->notify; + } + + /** + * Set whether the comment associated with the acknowledgement is persistent + * + * @param bool $persistent + * + * @return $this + */ + public function setPersistent($persistent = true) + { + $this->persistent = (bool) $persistent; + return $this; + } + + /** + * Is the comment associated with the acknowledgement is persistent? + * + * @return bool + */ + public function getPersistent() + { + return $this->persistent; + } + + /** + * Set the time when the acknowledgement should expire + * + * @param int $expireTime + * + * @return $this + */ + public function setExpireTime($expireTime) + { + $this->expireTime = (int) $expireTime; + return $this; + } + + /** + * Get the time when the acknowledgement should expire + * + * @return int|null + */ + public function getExpireTime() + { + return $this->expireTime; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php b/modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php new file mode 100644 index 0000000..9e3151f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php @@ -0,0 +1,80 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Add a comment to a host or service + */ +class AddCommentCommand extends WithCommentCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST, + self::TYPE_SERVICE + ); + + /** + * Whether the comment is persistent + * + * Persistent comments are not lost the next time the monitoring host restarts. + */ + protected $persistent; + + /** + * Optional time when the acknowledgement should expire + * + * @var int|null + */ + protected $expireTime; + + /** + * Set whether the comment is persistent + * + * @param bool $persistent + * + * @return $this + */ + public function setPersistent($persistent = true) + { + $this->persistent = $persistent; + return $this; + } + + /** + * Is the comment persistent? + * + * @return bool + */ + public function getPersistent() + { + return $this->persistent; + } + + /** + * Set the time when the acknowledgement should expire + * + * @param int $expireTime + * + * @return $this + */ + public function setExpireTime($expireTime) + { + $this->expireTime = (int) $expireTime; + + return $this; + } + + /** + * Get the time when the acknowledgement should expire + * + * @return int|null + */ + public function getExpireTime() + { + return $this->expireTime; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php new file mode 100644 index 0000000..6495375 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Schedule host downtime command for API command transport and Icinga >= 2.11.0 that + * sends all_services and child_options in a single request + */ +class ApiScheduleHostDowntimeCommand extends ScheduleHostDowntimeCommand +{ + /** @var int Whether no, triggered, or non-triggered child downtimes should be scheduled */ + protected $childOptions; + + protected $forAllServicesNative = true; + + /** + * Get child options, i.e. whether no, triggered, or non-triggered child downtimes should be scheduled + * + * @return int + */ + public function getChildOptions() + { + return $this->childOptions; + } + + /** + * Set child options, i.e. whether no, triggered, or non-triggered child downtimes should be scheduled + * + * @param int $childOptions + * + * @return $this + */ + public function setChildOptions($childOptions) + { + $this->childOptions = $childOptions; + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php b/modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php new file mode 100644 index 0000000..577e3df --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php @@ -0,0 +1,38 @@ +<?php + +/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +trait CommandAuthor +{ + /** + * Author of the command + * + * @var string + */ + protected $author; + + /** + * Set the author + * + * @param string $author + * + * @return $this + */ + public function setAuthor($author) + { + $this->author = (string) $author; + return $this; + } + + /** + * Get the author + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php b/modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php new file mode 100644 index 0000000..348175a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php @@ -0,0 +1,110 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +use Icinga\Module\Monitoring\Command\IcingaCommand; + +/** + * Delete a host or service comment + */ +class DeleteCommentCommand extends IcingaCommand +{ + use CommandAuthor; + + /** + * ID of the comment that is to be deleted + * + * @var int + */ + protected $commentId; + + /** + * Name of the comment (Icinga 2.4+) + * + * Required for removing the comment via Icinga 2's API. + * + * @var string + */ + protected $commentName; + + /** + * Whether the command affects a service comment + * + * @var boolean + */ + protected $isService = false; + + /** + * Get the ID of the comment that is to be deleted + * + * @return int + */ + public function getCommentId() + { + return $this->commentId; + } + + /** + * Set the ID of the comment that is to be deleted + * + * @param int $commentId + * + * @return $this + */ + public function setCommentId($commentId) + { + $this->commentId = (int) $commentId; + return $this; + } + + /** + * Get the name of the comment (Icinga 2.4+) + * + * Required for removing the comment via Icinga 2's API. + * + * @return string + */ + public function getCommentName() + { + return $this->commentName; + } + + /** + * Set the name of the comment (Icinga 2.4+) + * + * Required for removing the comment via Icinga 2's API. + * + * @param string $commentName + * + * @return $this + */ + public function setCommentName($commentName) + { + $this->commentName = $commentName; + return $this; + } + + /** + * Get whether the command affects a service comment + * + * @return boolean + */ + public function getIsService() + { + return $this->isService; + } + + /** + * Set whether the command affects a service comment + * + * @param bool $isService + * + * @return $this + */ + public function setIsService($isService = true) + { + $this->isService = (bool) $isService; + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php new file mode 100644 index 0000000..a314864 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php @@ -0,0 +1,110 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +use Icinga\Module\Monitoring\Command\IcingaCommand; + +/** + * Delete a host or service downtime + */ +class DeleteDowntimeCommand extends IcingaCommand +{ + use CommandAuthor; + + /** + * ID of the downtime that is to be deleted + * + * @var int + */ + protected $downtimeId; + + /** + * Name of the downtime (Icinga 2.4+) + * + * Required for removing the downtime via Icinga 2's API. + * + * @var string + */ + protected $downtimeName; + + /** + * Whether the command affects a service downtime + * + * @var boolean + */ + protected $isService = false; + + /** + * Get the ID of the downtime that is to be deleted + * + * @return int + */ + public function getDowntimeId() + { + return $this->downtimeId; + } + + /** + * Set the ID of the downtime that is to be deleted + * + * @param int $downtimeId + * + * @return $this + */ + public function setDowntimeId($downtimeId) + { + $this->downtimeId = (int) $downtimeId; + return $this; + } + + /** + * Get the name of the downtime (Icinga 2.4+) + * + * Required for removing the downtime via Icinga 2's API. + * + * @return string + */ + public function getDowntimeName() + { + return $this->downtimeName; + } + + /** + * Set the name of the downtime (Icinga 2.4+) + * + * Required for removing the downtime via Icinga 2's API. + * + * @param string $downtimeName + * + * @return $this + */ + public function setDowntimeName($downtimeName) + { + $this->downtimeName = $downtimeName; + return $this; + } + + /** + * Get whether the command affects a service + * + * @return bool + */ + public function getIsService() + { + return $this->isService; + } + + /** + * Set whether the command affects a service + * + * @param bool $isService + * + * @return $this + */ + public function setIsService($isService = true) + { + $this->isService = (bool) $isService; + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php new file mode 100644 index 0000000..43ab645 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php @@ -0,0 +1,61 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +use Icinga\Module\Monitoring\Command\IcingaCommand; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +/** + * Base class for commands that involve a monitored object, i.e. a host or service + */ +abstract class ObjectCommand extends IcingaCommand +{ + /** + * Type host + */ + const TYPE_HOST = MonitoredObject::TYPE_HOST; + + /** + * Type service + */ + const TYPE_SERVICE = MonitoredObject::TYPE_SERVICE; + + /** + * Allowed Icinga object types for the command + * + * @var string[] + */ + protected $allowedObjects = array(); + + /** + * Involved object + * + * @var MonitoredObject + */ + protected $object; + + /** + * Set the involved object + * + * @param MonitoredObject $object + * + * @return $this + */ + public function setObject(MonitoredObject $object) + { + $object->assertOneOf($this->allowedObjects); + $this->object = $object; + return $this; + } + + /** + * Get the involved object + * + * @return MonitoredObject + */ + public function getObject() + { + return $this->object; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php new file mode 100644 index 0000000..cd2db33 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php @@ -0,0 +1,176 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +use InvalidArgumentException; +use LogicException; + +/** + * Submit a passive check result for a host or service + */ +class ProcessCheckResultCommand extends ObjectCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST, + self::TYPE_SERVICE + ); + + /** + * Host up + */ + const HOST_UP = 0; + + /** + * Host down + */ + const HOST_DOWN = 1; + + /** + * Host unreachable + */ + const HOST_UNREACHABLE = 2; // TODO: Icinga 2.x does not support submitting results with this state, yet + + /** + * Service ok + */ + const SERVICE_OK = 0; + + /** + * Service warning + */ + const SERVICE_WARNING = 1; + + /** + * Service critical + */ + const SERVICE_CRITICAL = 2; + + /** + * Service unknown + */ + const SERVICE_UNKNOWN = 3; + + /** + * Possible status codes for passive host and service checks + * + * @var array + */ + public static $statusCodes = array( + self::TYPE_HOST => array( + self::HOST_UP, self::HOST_DOWN, self::HOST_UNREACHABLE + ), + self::TYPE_SERVICE => array( + self::SERVICE_OK, self::SERVICE_WARNING, self::SERVICE_CRITICAL, self::SERVICE_UNKNOWN + ) + ); + + /** + * Status code of the host or service check result + * + * @var int + */ + protected $status; + + /** + * Text output of the host or service check result + * + * @var string + */ + protected $output; + + /** + * Optional performance data of the host or service check result + * + * @var string + */ + protected $performanceData; + + + /** + * Set the status code of the host or service check result + * + * @param int $status + * + * @return $this + * + * @throws LogicException If the object is null + * @throws InvalidArgumentException If status is not one of the valid status codes for the object's type + */ + public function setStatus($status) + { + if ($this->object === null) { + throw new LogicException('You\'re required to call setObject() before calling setStatus()'); + } + $status = (int) $status; + if (! in_array($status, self::$statusCodes[$this->object->getType()])) { + throw new InvalidArgumentException(sprintf( + 'The status code %u you provided is not one of the valid status codes for type %s', + $status, + $this->object->getType() + )); + } + $this->status = $status; + return $this; + } + + /** + * Get the status code of the host or service check result + * + * @return int + */ + public function getStatus() + { + return $this->status; + } + + /** + * Set the text output of the host or service check result + * + * @param string $output + * + * @return $this + */ + public function setOutput($output) + { + $this->output = (string) $output; + return $this; + } + + /** + * Get the text output of the host or service check result + * + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Set the performance data of the host or service check result + * + * @param string $performanceData + * + * @return $this + */ + public function setPerformanceData($performanceData) + { + $this->performanceData = (string) $performanceData; + return $this; + } + + /** + * Get the performance data of the host or service check result + * + * @return string + */ + public function getPerformanceData() + { + return $this->performanceData; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php new file mode 100644 index 0000000..3fd350c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Schedule and propagate host downtime + */ +class PropagateHostDowntimeCommand extends ScheduleServiceDowntimeCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST + ); + + /** + * Whether the downtime for child hosts are all set to be triggered by this' host downtime + * + * @var bool + */ + protected $triggered = false; + + /** + * Set whether the downtime for child hosts are all set to be triggered by this' host downtime + * + * @param bool $triggered + * + * @return $this + */ + public function setTriggered($triggered = true) + { + $this->triggered = (bool) $triggered; + return $this; + } + + /** + * Get whether the downtime for child hosts are all set to be triggered by this' host downtime + * + * @return bool + */ + public function getTriggered() + { + return $this->triggered; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php b/modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php new file mode 100644 index 0000000..31c8180 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php @@ -0,0 +1,21 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Remove a problem acknowledgement from a host or service + */ +class RemoveAcknowledgementCommand extends ObjectCommand +{ + use CommandAuthor; + + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST, + self::TYPE_SERVICE + ); +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php new file mode 100644 index 0000000..8a0a2cb --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Schedule a host check + */ +class ScheduleHostCheckCommand extends ScheduleServiceCheckCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST + ); + + /** + * Whether to schedule a check of all services associated with a particular host + * + * @var bool + */ + protected $ofAllServices = false; + + /** + * Set whether to schedule a check of all services associated with a particular host + * + * @param bool $ofAllServices + * + * @return $this + */ + public function setOfAllServices($ofAllServices = true) + { + $this->ofAllServices = (bool) $ofAllServices; + return $this; + } + + /** + * Get whether to schedule a check of all services associated with a particular host + * + * @return bool + */ + public function getOfAllServices() + { + return $this->ofAllServices; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php new file mode 100644 index 0000000..3ac37d3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php @@ -0,0 +1,75 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Schedule a host downtime + */ +class ScheduleHostDowntimeCommand extends ScheduleServiceDowntimeCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST + ); + + /** + * Whether to schedule a downtime for all services associated with a particular host + * + * @var bool + */ + protected $forAllServices = false; + + /** @var bool Whether to send the all_services API parameter */ + protected $forAllServicesNative; + + /** + * Set whether to schedule a downtime for all services associated with a particular host + * + * @param bool $forAllServices + * + * @return $this + */ + public function setForAllServices($forAllServices = true) + { + $this->forAllServices = (bool) $forAllServices; + return $this; + } + + /** + * Get whether to schedule a downtime for all services associated with a particular host + * + * @return bool + */ + public function getForAllServices() + { + return $this->forAllServices; + } + + /** + * Get whether to send the all_services API parameter + * + * @return bool + */ + public function isForAllServicesNative() + { + return $this->forAllServicesNative; + } + + /** + * Get whether to send the all_services API parameter + * + * @param bool $forAllServicesNative + * + * @return $this + */ + public function setForAllServicesNative($forAllServicesNative = true) + { + $this->forAllServicesNative = (bool) $forAllServicesNative; + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php new file mode 100644 index 0000000..8880984 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php @@ -0,0 +1,92 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Schedule a service check + */ +class ScheduleServiceCheckCommand extends ObjectCommand +{ + /** + * {@inheritdoc} + */ + protected $allowedObjects = array( + self::TYPE_SERVICE + ); + + /** + * Time when the next check of a host or service is to be scheduled + * + * If active checks are disabled on a host- or service-specific or program-wide basis or the host or service is + * already scheduled to be checked at an earlier time, etc. The check may not actually be scheduled at the time + * specified. This behaviour can be overridden by setting `ScheduledCheck::$forced' to true. + * + * @var int Unix timestamp + */ + protected $checkTime; + + /** + * Whether the check is forced + * + * Forced checks are performed regardless of what time it is (e.g. time period restrictions are ignored) and whether + * or not active checks are enabled on a host- or service-specific or program-wide basis. + * + * @var bool + */ + protected $forced = false; + + /** + * Set the time when the next check of a host or service is to be scheduled + * + * @param int $checkTime Unix timestamp + * + * @return $this + */ + public function setCheckTime($checkTime) + { + $this->checkTime = (int) $checkTime; + return $this; + } + + /** + * Get the time when the next check of a host or service is to be scheduled + * + * @return int Unix timestamp + */ + public function getCheckTime() + { + return $this->checkTime; + } + + /** + * Set whether the check is forced + * + * @param bool $forced + * + * @return $this + */ + public function setForced($forced = true) + { + $this->forced = (bool) $forced; + return $this; + } + + /** + * Get whether the check is forced + * + * @return bool + */ + public function getForced() + { + return $this->forced; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'ScheduleCheck'; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php new file mode 100644 index 0000000..a023ab5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php @@ -0,0 +1,190 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Schedule a service downtime + */ +class ScheduleServiceDowntimeCommand extends AddCommentCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_SERVICE + ); + + /** + * Downtime starts at the exact time specified + * + * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a + * host or service transitions to a problem state determines the time at which the downtime actually starts. + * The downtime will then last for `Downtime::$duration' seconds. + * + * @var int Unix timestamp + */ + protected $start; + + /** + * Downtime ends at the exact time specified + * + * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a + * host or service transitions to a problem state determines the time at which the downtime actually starts. + * The downtime will then last for `Downtime::$duration' seconds. + * + * @var int Unix timestamp + */ + protected $end; + + /** + * Whether it's a fixed or flexible downtime + * + * @var bool + */ + protected $fixed = true; + + /** + * ID of the downtime which triggers this downtime + * + * The start of this downtime is triggered by the start of the other scheduled host or service downtime. + * + * @var int|null + */ + protected $triggerId; + + /** + * The duration in seconds the downtime must last if it's a flexible downtime + * + * If `Downtime::$fixed' is set to false, the downtime will last for the duration in seconds specified, even + * if the host or service recovers before the downtime expires. + * + * @var int|null + */ + protected $duration; + + /** + * Set the time when the downtime should start + * + * @param int $start Unix timestamp + * + * @return $this + */ + public function setStart($start) + { + $this->start = (int) $start; + return $this; + } + + /** + * Get the time when the downtime should start + * + * @return int Unix timestamp + */ + public function getStart() + { + return $this->start; + } + + /** + * Set the time when the downtime should end + * + * @param int $end Unix timestamp + * + * @return $this + */ + public function setEnd($end) + { + $this->end = (int) $end; + return $this; + } + + /** + * Get the time when the downtime should end + * + * @return int Unix timestamp + */ + public function getEnd() + { + return $this->end; + } + + /** + * Set whether it's a fixed or flexible downtime + * + * @param boolean $fixed + * + * @return $this + */ + public function setFixed($fixed = true) + { + $this->fixed = (bool) $fixed; + return $this; + } + + /** + * Is the downtime fixed? + * + * @return boolean + */ + public function getFixed() + { + return $this->fixed; + } + + /** + * Set the ID of the downtime which triggers this downtime + * + * @param int $triggerId + * + * @return $this + */ + public function setTriggerId($triggerId) + { + $this->triggerId = (int) $triggerId; + return $this; + } + + /** + * Get the ID of the downtime which triggers this downtime + * + * @return int|null + */ + public function getTriggerId() + { + return $this->triggerId; + } + + /** + * Set the duration in seconds the downtime must last if it's a flexible downtime + * + * @param int $duration + * + * @return $this + */ + public function setDuration($duration) + { + $this->duration = (int) $duration; + return $this; + } + + /** + * Get the duration in seconds the downtime must last if it's a flexible downtime + * + * @return int|null + */ + public function getDuration() + { + return $this->duration; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\IcingaCommand::getName() For the method documentation. + */ + public function getName() + { + return 'ScheduleDowntime'; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php b/modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php new file mode 100644 index 0000000..ac8889c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php @@ -0,0 +1,82 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Send custom notifications for a host or service + */ +class SendCustomNotificationCommand extends WithCommentCommand +{ + /** + * {@inheritdoc} + */ + protected $allowedObjects = array( + self::TYPE_HOST, + self::TYPE_SERVICE + ); + + /** + * Whether the notification is forced + * + * Forced notifications are sent out regardless of time restrictions and whether or not notifications are enabled. + * + * @var bool + */ + protected $forced; + + /** + * Whether to broadcast the notification + * + * Broadcast notifications are sent out to all normal and escalated contacts. + * + * @var bool + */ + protected $broadcast; + + /** + * Get whether to force the notification + * + * @return bool + */ + public function getForced() + { + return $this->forced; + } + + /** + * Set whether to force the notification + * + * @param bool $forced + * + * @return $this + */ + public function setForced($forced = true) + { + $this->forced = $forced; + return $this; + } + + /** + * Get whether to broadcast the notification + * + * @return bool + */ + public function getBroadcast() + { + return $this->broadcast; + } + + /** + * Set whether to broadcast the notification + * + * @param bool $broadcast + * + * @return $this + */ + public function setBroadcast($broadcast = true) + { + $this->broadcast = $broadcast; + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php new file mode 100644 index 0000000..e3ba8a2 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php @@ -0,0 +1,113 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Enable or disable a feature of an Icinga object, i.e. host or service + */ +class ToggleObjectFeatureCommand extends ObjectCommand +{ + /** + * (non-PHPDoc) + * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation. + */ + protected $allowedObjects = array( + self::TYPE_HOST, + self::TYPE_SERVICE + ); + + /** + * Feature for enabling or disabling active checks of a host or service + */ + const FEATURE_ACTIVE_CHECKS = 'active_checks_enabled'; + + /** + * Feature for enabling or disabling passive checks of a host or service + */ + const FEATURE_PASSIVE_CHECKS = 'passive_checks_enabled'; + + /** + * Feature for enabling or disabling processing of host or service checks via the OCHP command for a host or service + */ + const FEATURE_OBSESSING = 'obsessing'; + + /** + * Feature for enabling or disabling notifications for a host or service + * + * Notifications will be sent out only if notifications are enabled on a program-wide basis as well. + */ + const FEATURE_NOTIFICATIONS = 'notifications_enabled'; + + /** + * Feature for enabling or disabling event handler for a host or service + */ + const FEATURE_EVENT_HANDLER = 'event_handler_enabled'; + + /** + * Feature for enabling or disabling flap detection for a host or service. + * + * In order to enable flap detection flap detection must be enabled on a program-wide basis as well. + */ + const FEATURE_FLAP_DETECTION = 'flap_detection_enabled'; + + /** + * Feature that is to be enabled or disabled + * + * @var string + */ + protected $feature; + + /** + * Whether the feature should be enabled or disabled + * + * @var bool + */ + protected $enabled; + + /** + * Set the feature that is to be enabled or disabled + * + * @param string $feature + * + * @return $this + */ + public function setFeature($feature) + { + $this->feature = (string) $feature; + return $this; + } + + /** + * Get the feature that is to be enabled or disabled + * + * @return string + */ + public function getFeature() + { + return $this->feature; + } + + /** + * Set whether the feature should be enabled or disabled + * + * @param bool $enabled + * + * @return $this + */ + public function setEnabled($enabled = true) + { + $this->enabled = (bool) $enabled; + return $this; + } + + /** + * Get whether the feature should be enabled or disabled + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php b/modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php new file mode 100644 index 0000000..aa2e439 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Object; + +/** + * Base class for commands adding comments + */ +abstract class WithCommentCommand extends ObjectCommand +{ + use CommandAuthor; + + /** + * Comment + * + * @var string + */ + protected $comment; + + /** + * Set the comment + * + * @param string $comment + * + * @return $this + */ + public function setComment($comment) + { + $this->comment = (string) $comment; + return $this; + } + + /** + * Get the comment + * + * @return string + */ + public function getComment() + { + return $this->comment; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php new file mode 100644 index 0000000..8370314 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php @@ -0,0 +1,324 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Renderer; + +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Command\IcingaApiCommand; +use Icinga\Module\Monitoring\Command\Instance\ToggleInstanceFeatureCommand; +use Icinga\Module\Monitoring\Command\Object\AcknowledgeProblemCommand; +use Icinga\Module\Monitoring\Command\Object\AddCommentCommand; +use Icinga\Module\Monitoring\Command\Object\ApiScheduleHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand; +use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\ProcessCheckResultCommand; +use Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\RemoveAcknowledgementCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\SendCustomNotificationCommand; +use Icinga\Module\Monitoring\Command\Object\ToggleObjectFeatureCommand; +use Icinga\Module\Monitoring\Command\IcingaCommand; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use InvalidArgumentException; + +/** + * Icinga command renderer for the Icinga command file + */ +class IcingaApiCommandRenderer implements IcingaCommandRendererInterface +{ + /** + * Name of the Icinga application object + * + * @var string + */ + protected $app = 'app'; + + /** + * Get the name of the Icinga application object + * + * @return string + */ + public function getApp() + { + return $this->app; + } + + /** + * Set the name of the Icinga application object + * + * @param string $app + * + * @return $this + */ + public function setApp($app) + { + $this->app = $app; + + return $this; + } + + /** + * Apply filter to query data + * + * @param array $data + * @param MonitoredObject $object + * + * @return array + */ + protected function applyFilter(array &$data, MonitoredObject $object) + { + if ($object->getType() === $object::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $data['host'] = $object->getName(); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $data['service'] = sprintf('%s!%s', $object->getHost()->getName(), $object->getName()); + } + } + + /** + * Render a command + * + * @param IcingaCommand $command + * + * @return IcingaApiCommand + */ + public function render(IcingaCommand $command) + { + $renderMethod = 'render' . $command->getName(); + if (! method_exists($this, $renderMethod)) { + die($renderMethod); + } + return $this->$renderMethod($command); + } + + public function renderAddComment(AddCommentCommand $command) + { + $endpoint = 'actions/add-comment'; + $data = array( + 'author' => $command->getAuthor(), + 'comment' => $command->getComment() + ); + + if ($command->getExpireTime() !== null) { + $data['expiry'] = $command->getExpireTime(); + } + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderSendCustomNotification(SendCustomNotificationCommand $command) + { + $endpoint = 'actions/send-custom-notification'; + $data = array( + 'author' => $command->getAuthor(), + 'comment' => $command->getComment(), + 'force' => $command->getForced() + ); + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderProcessCheckResult(ProcessCheckResultCommand $command) + { + $endpoint = 'actions/process-check-result'; + $data = array( + 'exit_status' => $command->getStatus(), + 'plugin_output' => $command->getOutput(), + 'performance_data' => $command->getPerformanceData() + ); + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderScheduleCheck(ScheduleServiceCheckCommand $command) + { + $endpoint = 'actions/reschedule-check'; + $data = array( + 'next_check' => $command->getCheckTime(), + 'force' => $command->getForced() + ); + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderScheduleDowntime(ScheduleServiceDowntimeCommand $command) + { + $endpoint = 'actions/schedule-downtime'; + $data = array( + 'author' => $command->getAuthor(), + 'comment' => $command->getComment(), + 'start_time' => $command->getStart(), + 'end_time' => $command->getEnd(), + 'duration' => $command->getDuration(), + 'fixed' => $command->getFixed(), + 'trigger_name' => $command->getTriggerId() + ); + $commandData = $data; + + if ($command instanceof PropagateHostDowntimeCommand) { + $commandData['child_options'] = $command->getTriggered() ? 1 : 2; + } elseif ($command instanceof ApiScheduleHostDowntimeCommand) { + // We assume that it has previously been verified that the Icinga version is + // equal to or greater than 2.11.0 + $commandData['child_options'] = $command->getChildOptions(); + } + + $allServicesCompat = false; + if ($command instanceof ScheduleHostDowntimeCommand) { + if ($command->isForAllServicesNative()) { + // We assume that it has previously been verified that the Icinga version is + // equal to or greater than 2.11.0 + $commandData['all_services'] = $command->getForAllServices(); + } else { + $allServicesCompat = $command->getForAllServices(); + } + } + + $this->applyFilter($commandData, $command->getObject()); + $apiCommand = IcingaApiCommand::create($endpoint, $commandData); + + if ($allServicesCompat) { + $commandData = $data + [ + 'type' => 'Service', + 'filter' => 'host.name == host_name', + 'filter_vars' => [ + 'host_name' => $command->getObject()->getName() + ] + ]; + $apiCommand->setNext(IcingaApiCommand::create($endpoint, $commandData)); + } + + return $apiCommand; + } + + public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command) + { + $endpoint = 'actions/acknowledge-problem'; + $data = array( + 'author' => $command->getAuthor(), + 'comment' => $command->getComment(), + 'sticky' => $command->getSticky(), + 'notify' => $command->getNotify(), + 'persistent' => $command->getPersistent() + ); + if ($command->getExpireTime() !== null) { + $data['expiry'] = $command->getExpireTime(); + } + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command) + { + if ($command->getEnabled() === true) { + $enabled = true; + } else { + $enabled = false; + } + switch ($command->getFeature()) { + case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS: + $attr = 'enable_active_checks'; + break; + case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS: + $attr = 'enable_passive_checks'; + break; + case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS: + $attr = 'enable_notifications'; + break; + case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER: + $attr = 'enable_event_handler'; + break; + case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION: + $attr = 'enable_flapping'; + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + $endpoint = 'objects/'; + $object = $command->getObject(); + if ($object->getType() === ToggleObjectFeatureCommand::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $endpoint .= 'hosts'; + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $endpoint .= 'services'; + } + $data = array( + 'attrs' => array( + $attr => $enabled + ) + ); + $this->applyFilter($data, $object); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderDeleteComment(DeleteCommentCommand $command) + { + $endpoint = 'actions/remove-comment'; + $data = [ + 'author' => $command->getAuthor(), + 'comment' => $command->getCommentName() + ]; + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderDeleteDowntime(DeleteDowntimeCommand $command) + { + $endpoint = 'actions/remove-downtime'; + $data = [ + 'author' => $command->getAuthor(), + 'downtime' => $command->getDowntimeName() + ]; + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command) + { + $endpoint = 'actions/remove-acknowledgement'; + $data = ['author' => $command->getAuthor()]; + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command) + { + $endpoint = 'objects/icingaapplications/' . $this->getApp(); + if ($command->getEnabled() === true) { + $enabled = true; + } else { + $enabled = false; + } + switch ($command->getFeature()) { + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: + $attr = 'enable_host_checks'; + break; + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: + $attr = 'enable_service_checks'; + break; + case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: + $attr = 'enable_event_handlers'; + break; + case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: + $attr = 'enable_flapping'; + break; + case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: + $attr = 'enable_notifications'; + break; + case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: + $attr = 'enable_perfdata'; + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + $data = array( + 'attrs' => array( + $attr => $enabled + ) + ); + return IcingaApiCommand::create($endpoint, $data); + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php new file mode 100644 index 0000000..97d1314 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php @@ -0,0 +1,478 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Renderer; + +use Icinga\Module\Monitoring\Command\Instance\DisableNotificationsExpireCommand; +use Icinga\Module\Monitoring\Command\Instance\ToggleInstanceFeatureCommand; +use Icinga\Module\Monitoring\Command\Object\AcknowledgeProblemCommand; +use Icinga\Module\Monitoring\Command\Object\AddCommentCommand; +use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand; +use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\ProcessCheckResultCommand; +use Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\RemoveAcknowledgementCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand; +use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand; +use Icinga\Module\Monitoring\Command\Object\SendCustomNotificationCommand; +use Icinga\Module\Monitoring\Command\Object\ToggleObjectFeatureCommand; +use Icinga\Module\Monitoring\Command\IcingaCommand; +use InvalidArgumentException; + +/** + * Icinga command renderer for the Icinga command file + */ +class IcingaCommandFileCommandRenderer implements IcingaCommandRendererInterface +{ + /** + * Escape a command string + * + * @param string $commandString + * + * @return string + */ + protected function escape($commandString) + { + return str_replace(array("\r", "\n"), array('\r', '\n'), $commandString); + } + + /** + * Render a command + * + * @param IcingaCommand $command + * @param int|null $now + * + * @return string + */ + public function render(IcingaCommand $command, $now = null) + { + $renderMethod = 'render' . $command->getName(); + if (! method_exists($this, $renderMethod)) { + die($renderMethod); + } + if ($now === null) { + $now = time(); + } + return sprintf('[%u] %s', $now, $this->escape($this->$renderMethod($command))); + } + + public function renderAddComment(AddCommentCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $commandString = sprintf( + 'ADD_HOST_COMMENT;%s', + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + 'ADD_SVC_COMMENT;%s;%s', + $object->getHost()->getName(), + $object->getName() + ); + } + return sprintf( + '%s;%u;%s;%s', + $commandString, + $command->getPersistent(), + $command->getAuthor(), + $command->getComment() + ); + } + + public function renderSendCustomNotification(SendCustomNotificationCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $commandString = sprintf( + 'SEND_CUSTOM_HOST_NOTIFICATION;%s', + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + 'SEND_CUSTOM_SVC_NOTIFICATION;%s;%s', + $object->getHost()->getName(), + $object->getName() + ); + } + $options = 0; // 0 for no options + if ($command->getBroadcast() === true) { + $options |= 1; + } + if ($command->getForced() === true) { + $options |= 2; + } + return sprintf( + '%s;%u;%s;%s', + $commandString, + $options, + $command->getAuthor(), + $command->getComment() + ); + } + + public function renderProcessCheckResult(ProcessCheckResultCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $commandString = sprintf( + 'PROCESS_HOST_CHECK_RESULT;%s', + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + 'PROCESS_SERVICE_CHECK_RESULT;%s;%s', + $object->getHost()->getName(), + $object->getName() + ); + } + $output = $command->getOutput(); + if ($command->getPerformanceData() !== null) { + $output .= '|' . $command->getPerformanceData(); + } + return sprintf( + '%s;%u;%s', + $commandString, + $command->getStatus(), + $output + ); + } + + public function renderScheduleCheck(ScheduleServiceCheckCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + /** @var \Icinga\Module\Monitoring\Command\Object\ScheduleHostCheckCommand $command */ + if ($command->getOfAllServices() === true) { + if ($command->getForced() === true) { + $commandName = 'SCHEDULE_FORCED_HOST_SVC_CHECKS'; + } else { + $commandName = 'SCHEDULE_HOST_SVC_CHECKS'; + } + } else { + if ($command->getForced() === true) { + $commandName = 'SCHEDULE_FORCED_HOST_CHECK'; + } else { + $commandName = 'SCHEDULE_HOST_CHECK'; + } + } + $commandString = sprintf( + '%s;%s', + $commandName, + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + '%s;%s;%s', + $command->getForced() === true ? 'SCHEDULE_FORCED_SVC_CHECK' : 'SCHEDULE_SVC_CHECK', + $object->getHost()->getName(), + $object->getName() + ); + } + return sprintf( + '%s;%u', + $commandString, + $command->getCheckTime() + ); + } + + public function renderScheduleDowntime(ScheduleServiceDowntimeCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + /** @var \Icinga\Module\Monitoring\Command\Object\ScheduleHostDowntimeCommand $command */ + if ($command instanceof PropagateHostDowntimeCommand) { + /** @var \Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand $command */ + $commandName = $command->getTriggered() === true ? 'SCHEDULE_AND_PROPAGATE_TRIGGERED_HOST_DOWNTIME' + : 'SCHEDULE_AND_PROPAGATE_HOST_DOWNTIME'; + } elseif ($command->getForAllServices() === true) { + $commandName = 'SCHEDULE_HOST_SVC_DOWNTIME'; + } else { + $commandName = 'SCHEDULE_HOST_DOWNTIME'; + } + $commandString = sprintf( + '%s;%s', + $commandName, + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + '%s;%s;%s', + 'SCHEDULE_SVC_DOWNTIME', + $object->getHost()->getName(), + $object->getName() + ); + } + return sprintf( + '%s;%u;%u;%u;%u;%u;%s;%s', + $commandString, + $command->getStart(), + $command->getEnd(), + $command->getFixed(), + $command->getTriggerId(), + $command->getDuration(), + $command->getAuthor(), + $command->getComment() + ); + } + + public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $commandString = sprintf( + '%s;%s', + $command->getExpireTime() !== null ? 'ACKNOWLEDGE_HOST_PROBLEM_EXPIRE' : 'ACKNOWLEDGE_HOST_PROBLEM', + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + '%s;%s;%s', + $command->getExpireTime() !== null ? 'ACKNOWLEDGE_SVC_PROBLEM_EXPIRE' : 'ACKNOWLEDGE_SVC_PROBLEM', + $object->getHost()->getName(), + $object->getName() + ); + } + $commandString = sprintf( + '%s;%u;%u;%u', + $commandString, + $command->getSticky() ? 2 : 0, + $command->getNotify(), + $command->getPersistent() + ); + if ($command->getExpireTime() !== null) { + $commandString = sprintf( + '%s;%u', + $commandString, + $command->getExpireTime() + ); + } + return sprintf( + '%s;%s;%s', + $commandString, + $command->getAuthor(), + $command->getComment() + ); + } + + public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command) + { + if ($command->getEnabled() === true) { + $commandPrefix = 'ENABLE'; + } else { + $commandPrefix = 'DISABLE'; + } + switch ($command->getFeature()) { + case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS: + $commandFormat = sprintf('%s_%%s_CHECK', $commandPrefix); + break; + case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS: + $commandFormat = sprintf('%s_PASSIVE_%%s_CHECKS', $commandPrefix); + break; + case ToggleObjectFeatureCommand::FEATURE_OBSESSING: + if ($command->getEnabled() === true) { + $commandPrefix = 'START'; + } else { + $commandPrefix = 'STOP'; + } + $commandFormat = sprintf('%s_OBSESSING_OVER_%%s', $commandPrefix); + break; + case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS: + $commandFormat = sprintf('%s_%%s_NOTIFICATIONS', $commandPrefix); + break; + case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER: + $commandFormat = sprintf('%s_%%s_EVENT_HANDLER', $commandPrefix); + break; + case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION: + $commandFormat = sprintf('%s_%%s_FLAP_DETECTION', $commandPrefix); + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + $object = $command->getObject(); + if ($object->getType() === ToggleObjectFeatureCommand::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $commandString = sprintf( + $commandFormat . ';%s', + 'HOST', + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + $commandFormat . ';%s;%s', + 'SVC', + $object->getHost()->getName(), + $object->getName() + ); + } + return $commandString; + } + + public function renderDeleteComment(DeleteCommentCommand $command) + { + return sprintf( + '%s;%u', + $command->getIsService() ? 'DEL_SVC_COMMENT' : 'DEL_HOST_COMMENT', + $command->getCommentId() + ); + } + + public function renderDeleteDowntime(DeleteDowntimeCommand $command) + { + return sprintf( + '%s;%u', + $command->getIsService() ? 'DEL_SVC_DOWNTIME' : 'DEL_HOST_DOWNTIME', + $command->getDowntimeId() + ); + } + + public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command) + { + $object = $command->getObject(); + if ($command->getObject()->getType() === $command::TYPE_HOST) { + /** @var \Icinga\Module\Monitoring\Object\Host $object */ + $commandString = sprintf( + '%s;%s', + 'REMOVE_HOST_ACKNOWLEDGEMENT', + $object->getName() + ); + } else { + /** @var \Icinga\Module\Monitoring\Object\Service $object */ + $commandString = sprintf( + '%s;%s;%s', + 'REMOVE_SVC_ACKNOWLEDGEMENT', + $object->getHost()->getName(), + $object->getName() + ); + } + return $commandString; + } + + public function renderDisableNotificationsExpire(DisableNotificationsExpireCommand $command) + { + return sprintf( + '%s;%u;%u', + 'DISABLE_NOTIFICATIONS_EXPIRE_TIME', + time(), + $command->getExpireTime() + ); + } + + public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command) + { + switch ($command->getFeature()) { + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: + case ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING: + case ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING: + case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS: + case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS: + if ($command->getEnabled() === true) { + $commandPrefix = 'START'; + } else { + $commandPrefix = 'STOP'; + } + break; + case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: + case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: + case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: + case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: + if ($command->getEnabled() === true) { + $commandPrefix = 'ENABLE'; + } else { + $commandPrefix = 'DISABLE'; + } + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + switch ($command->getFeature()) { + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'EXECUTING_HOST_CHECKS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'EXECUTING_SVC_CHECKS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'EVENT_HANDLERS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'FLAP_DETECTION' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'NOTIFICATIONS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'OBSESSING_OVER_HOST_CHECKS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'OBSESSING_OVER_SVC_CHECKS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'ACCEPTING_PASSIVE_HOST_CHECKS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'ACCEPTING_PASSIVE_SVC_CHECKS' + ); + break; + case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: + $commandString = sprintf( + '%s_%s', + $commandPrefix, + 'PERFORMANCE_DATA' + ); + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + return $commandString; + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php new file mode 100644 index 0000000..e3ef6ba --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Renderer; + +/** + * Interface for Icinga command renderer + */ +interface IcingaCommandRendererInterface +{ +} diff --git a/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php new file mode 100644 index 0000000..06e6afd --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php @@ -0,0 +1,291 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Transport; + +use Icinga\Application\Hook\AuditHook; +use Icinga\Application\Logger; +use Icinga\Exception\Json\JsonDecodeException; +use Icinga\Module\Monitoring\Command\IcingaApiCommand; +use Icinga\Module\Monitoring\Command\IcingaCommand; +use Icinga\Module\Monitoring\Command\Renderer\IcingaApiCommandRenderer; +use Icinga\Module\Monitoring\Exception\CommandTransportException; +use Icinga\Module\Monitoring\Exception\CurlException; +use Icinga\Module\Monitoring\Web\Rest\RestRequest; +use Icinga\Util\Json; + +/** + * Command transport over Icinga 2's REST API + */ +class ApiCommandTransport implements CommandTransportInterface +{ + /** + * Transport identifier + */ + const TRANSPORT = 'api'; + + /** + * API host + * + * @var string + */ + protected $host; + + /** + * API password + * + * @var string + */ + protected $password; + + /** + * API port + * + * @var int + */ + protected $port = 5665; + + /** + * Command renderer + * + * @var IcingaApiCommandRenderer + */ + protected $renderer; + + /** + * API username + * + * @var string + */ + protected $username; + + /** + * Create a new API command transport + */ + public function __construct() + { + $this->renderer = new IcingaApiCommandRenderer(); + } + + /** + * Set the name of the Icinga application object + * + * @param string $app + * + * @return $this + */ + public function setApp($app) + { + $this->renderer->setApp($app); + + return $this; + } + + /** + * Get the API host + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Set the API host + * + * @param string $host + * + * @return $this + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * Get the API password + * + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set the API password + * + * @param string $password + * + * @return $this + */ + public function setPassword($password) + { + $this->password = $password; + + return $this; + } + + /** + * Get the API port + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Set the API port + * + * @param int $port + * + * @return $this + */ + public function setPort($port) + { + $this->port = (int) $port; + + return $this; + } + + /** + * Get the API username + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Set the API username + * + * @param string $username + * + * @return $this + */ + public function setUsername($username) + { + $this->username = $username; + + return $this; + } + + /** + * Get URI for endpoint + * + * @param string $endpoint + * + * @return string + */ + protected function getUriFor($endpoint) + { + return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint); + } + + protected function sendCommand(IcingaApiCommand $command) + { + Logger::debug( + 'Sending Icinga command "%s" to the API "%s:%u"', + $command->getEndpoint(), + $this->getHost(), + $this->getPort() + ); + + $data = $command->getData(); + $payload = Json::encode($data); + AuditHook::logActivity( + 'monitoring/command', + "Issued command {$command->getEndpoint()} with the following payload: $payload", + $data + ); + + try { + $response = RestRequest::post($this->getUriFor($command->getEndpoint())) + ->authenticateWith($this->getUsername(), $this->getPassword()) + ->sendJson() + ->noStrictSsl() + ->setPayload($command->getData()) + ->send(); + } catch (JsonDecodeException $e) { + throw new CommandTransportException( + 'Got invalid JSON response from the Icinga 2 API: %s', + $e->getMessage() + ); + } + + if (isset($response['error'])) { + throw new CommandTransportException( + 'Can\'t send external Icinga command: %u %s', + $response['error'], + $response['status'] + ); + } + $result = array_pop($response['results']); + if (! empty($result) + && ($result['code'] < 200 || $result['code'] >= 300) + ) { + throw new CommandTransportException( + 'Can\'t send external Icinga command: %u %s', + $result['code'], + $result['status'] + ); + } + if ($command->hasNext()) { + $this->sendCommand($command->getNext()); + } + } + + /** + * Send the Icinga command over the Icinga 2 API + * + * @param IcingaCommand $command + * @param int|null $now + * + * @throws CommandTransportException + */ + public function send(IcingaCommand $command, $now = null) + { + $this->sendCommand($this->renderer->render($command)); + } + + /** + * Try to connect to the API + * + * @throws CommandTransportException In case of failure + */ + public function probe() + { + $request = RestRequest::get($this->getUriFor(null)) + ->authenticateWith($this->getUsername(), $this->getPassword()) + ->noStrictSsl(); + + try { + $response = $request->send(); + } catch (CurlException $e) { + throw new CommandTransportException( + 'Couldn\'t connect to the Icinga 2 API: %s', + $e->getMessage() + ); + } catch (JsonDecodeException $e) { + throw new CommandTransportException( + 'Got invalid JSON response from the Icinga 2 API: %s', + $e->getMessage() + ); + } + + if (isset($response['error'])) { + throw new CommandTransportException( + 'Can\'t connect to the Icinga 2 API: %u %s', + $response['error'], + $response['status'] + ); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php new file mode 100644 index 0000000..aa47547 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php @@ -0,0 +1,170 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Transport; + +use Exception; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Data\ConfigObject; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Monitoring\Command\IcingaCommand; +use Icinga\Module\Monitoring\Command\Object\ObjectCommand; +use Icinga\Module\Monitoring\Exception\CommandTransportException; + +/** + * Command transport + * + * This class is subject to change as we do not have environments yet (#4471). + */ +class CommandTransport implements CommandTransportInterface +{ + /** + * Transport configuration + * + * @var Config + */ + protected static $config; + + /** + * Get transport configuration + * + * @return Config + * + * @throws ConfigurationError + */ + public static function getConfig() + { + if (static::$config === null) { + $config = Config::module('monitoring', 'commandtransports'); + if ($config->isEmpty()) { + throw new ConfigurationError( + mt('monitoring', 'No command transports have been configured in "%s".'), + $config->getConfigFile() + ); + } + + static::$config = $config; + } + + return static::$config; + } + + /** + * Create a transport from config + * + * @param ConfigObject $config + * + * @return LocalCommandFile|RemoteCommandFile + * + * @throws ConfigurationError + */ + public static function createTransport(ConfigObject $config) + { + $config = clone $config; + switch (strtolower($config->transport)) { + case RemoteCommandFile::TRANSPORT: + $transport = new RemoteCommandFile(); + break; + case ApiCommandTransport::TRANSPORT: + $transport = new ApiCommandTransport(); + break; + case LocalCommandFile::TRANSPORT: + case '': // Casting null to string is the empty string + $transport = new LocalCommandFile(); + break; + default: + throw new ConfigurationError( + mt( + 'monitoring', + 'Cannot create command transport "%s". Invalid transport' + . ' defined in "%s". Use one of "%s", "%s" or "%s".' + ), + $config->transport, + static::getConfig()->getConfigFile(), + LocalCommandFile::TRANSPORT, + RemoteCommandFile::TRANSPORT, + ApiCommandTransport::TRANSPORT + ); + } + + unset($config->transport); + foreach ($config as $key => $value) { + $method = 'set' . ucfirst($key); + if (! method_exists($transport, $method)) { + // Ignore settings from config that don't have a setter on the transport instead of throwing an + // exception here because the transport should throw an exception if it's not fully set up + // when being about to send a command + continue; + } + + $transport->$method($value); + } + + return $transport; + } + + /** + * Send the given command over an appropriate Icinga command transport + * + * This will try one configured transport after another until the command has been successfully sent. + * + * @param IcingaCommand $command The command to send + * @param int|null $now Timestamp of the command or null for now + * + * @throws CommandTransportException If sending the Icinga command failed + */ + public function send(IcingaCommand $command, $now = null) + { + $errors = array(); + + foreach (static::getConfig() as $name => $transportConfig) { + $transport = static::createTransport($transportConfig); + if ($this->transferPossible($command, $transport)) { + try { + $transport->send($command, $now); + } catch (Exception $e) { + Logger::error($e); + $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.')); + continue; // Try the next transport + } + + return; // The command was successfully sent + } + } + + if (! empty($errors)) { + throw new CommandTransportException(implode("\n", $errors)); + } + + throw new CommandTransportException( + mt( + 'monitoring', + 'Failed to send external Icinga command. No transport has been configured' + . ' for this instance. Please contact your Icinga Web administrator.' + ) + ); + } + + /** + * Return whether it is possible to send the given command using the given transport + * + * @param IcingaCommand $command + * @param CommandTransportInterface $transport + * + * @return bool + */ + protected function transferPossible($command, $transport) + { + if (! method_exists($transport, 'getInstance') || !$command instanceof ObjectCommand) { + return true; + } + + $transportInstance = $transport->getInstance(); + if (! $transportInstance || $transportInstance === 'none') { + return true; + } + + return strtolower($transportInstance) === strtolower($command->getObject()->instance_name); + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php new file mode 100644 index 0000000..e9cb086 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php @@ -0,0 +1,22 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Transport; + +use Icinga\Module\Monitoring\Command\IcingaCommand; + +/** + * Interface for Icinga command transports + */ +interface CommandTransportInterface +{ + /** + * Send an Icinga command over the Icinga command transport + * + * @param IcingaCommand $command The command to send + * @param int|null $now Timestamp of the command or null for now + * + * @throws \Icinga\Module\Monitoring\Exception\CommandTransportException If sending the Icinga command failed + */ + public function send(IcingaCommand $command, $now = null); +} diff --git a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php new file mode 100644 index 0000000..891a46f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php @@ -0,0 +1,168 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Transport; + +use Exception; +use RuntimeException; +use Icinga\Application\Logger; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Monitoring\Command\IcingaCommand; +use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer; +use Icinga\Module\Monitoring\Exception\CommandTransportException; +use Icinga\Util\File; + +/** + * A local Icinga command file + */ +class LocalCommandFile implements CommandTransportInterface +{ + /** + * Transport identifier + */ + const TRANSPORT = 'local'; + + /** + * The name of the Icinga instance this transport will transfer commands to + * + * @var string + */ + protected $instanceName; + + /** + * Path to the icinga command file + * + * @var String + */ + protected $path; + + /** + * Mode used to open the icinga command file + * + * @var string + */ + protected $openMode = 'wn'; + + /** + * Command renderer + * + * @var IcingaCommandFileCommandRenderer + */ + protected $renderer; + + /** + * Create a new local command file command transport + */ + public function __construct() + { + $this->renderer = new IcingaCommandFileCommandRenderer(); + } + + /** + * Set the name of the Icinga instance this transport will transfer commands to + * + * @param string $name + * + * @return $this + */ + public function setInstance($name) + { + $this->instanceName = $name; + return $this; + } + + /** + * Return the name of the Icinga instance this transport will transfer commands to + * + * @return string + */ + public function getInstance() + { + return $this->instanceName; + } + + /** + * Set the path to the local Icinga command file + * + * @param string $path + * + * @return $this + */ + public function setPath($path) + { + $this->path = (string) $path; + return $this; + } + + /** + * Get the path to the local Icinga command file + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Set the mode used to open the icinga command file + * + * @param string $openMode + * + * @return $this + */ + public function setOpenMode($openMode) + { + $this->openMode = (string) $openMode; + return $this; + } + + /** + * Get the mode used to open the icinga command file + * + * @return string + */ + public function getOpenMode() + { + return $this->openMode; + } + + /** + * Write the command to the local Icinga command file + * + * @param IcingaCommand $command + * @param int|null $now + * + * @throws ConfigurationError + * @throws CommandTransportException + */ + public function send(IcingaCommand $command, $now = null) + { + if (! isset($this->path)) { + throw new ConfigurationError( + 'Can\'t send external Icinga Command. Path to the local command file is missing' + ); + } + $commandString = $this->renderer->render($command, $now); + Logger::debug( + 'Sending external Icinga command "%s" to the local command file "%s"', + $commandString, + $this->path + ); + try { + $file = new File($this->path, $this->openMode); + $file->fwrite($commandString . "\n"); + } catch (Exception $e) { + $message = $e->getMessage(); + if ($e instanceof RuntimeException && ($pos = strrpos($message, ':')) !== false) { + // Assume RuntimeException thrown by SplFileObject in the format: __METHOD__ . "({$filename}): Message" + $message = substr($message, $pos + 1); + } + throw new CommandTransportException( + 'Can\'t send external Icinga command to the local command file "%s": %s', + $this->path, + $message + ); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php new file mode 100644 index 0000000..5426bb9 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php @@ -0,0 +1,465 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Command\Transport; + +use Icinga\Application\Logger; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Monitoring\Command\IcingaCommand; +use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer; +use Icinga\Module\Monitoring\Exception\CommandTransportException; + +/** + * A remote Icinga command file + * + * Key-based SSH login must be possible for the user to log in as on the remote host + */ +class RemoteCommandFile implements CommandTransportInterface +{ + /** + * Transport identifier + */ + const TRANSPORT = 'remote'; + + /** + * The name of the Icinga instance this transport will transfer commands to + * + * @var string + */ + protected $instanceName; + + /** + * Remote host + * + * @var string + */ + protected $host; + + /** + * Port to connect to on the remote host + * + * @var int + */ + protected $port = 22; + + /** + * User to log in as on the remote host + * + * Defaults to current PHP process' user + * + * @var string + */ + protected $user; + + /** + * Path to the private key file for the key-based authentication + * + * @var string + */ + protected $privateKey; + + /** + * Path to the Icinga command file on the remote host + * + * @var string + */ + protected $path; + + /** + * Command renderer + * + * @var IcingaCommandFileCommandRenderer + */ + protected $renderer; + + /** + * SSH subprocess pipes + * + * @var array + */ + protected $sshPipes; + + /** + * SSH subprocess + * + * @var resource + */ + protected $sshProcess; + + /** + * Create a new remote command file command transport + */ + public function __construct() + { + $this->renderer = new IcingaCommandFileCommandRenderer(); + } + + /** + * Set the name of the Icinga instance this transport will transfer commands to + * + * @param string $name + * + * @return $this + */ + public function setInstance($name) + { + $this->instanceName = $name; + return $this; + } + + /** + * Return the name of the Icinga instance this transport will transfer commands to + * + * @return string + */ + public function getInstance() + { + return $this->instanceName; + } + + /** + * Set the remote host + * + * @param string $host + * + * @return $this + */ + public function setHost($host) + { + $this->host = (string) $host; + return $this; + } + + /** + * Get the remote host + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Set the port to connect to on the remote host + * + * @param int $port + * + * @return $this + */ + public function setPort($port) + { + $this->port = (int) $port; + return $this; + } + + /** + * Get the port to connect on the remote host + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Set the user to log in as on the remote host + * + * @param string $user + * + * @return $this + */ + public function setUser($user) + { + $this->user = (string) $user; + return $this; + } + + /** + * Get the user to log in as on the remote host + * + * Defaults to current PHP process' user + * + * @return string|null + */ + public function getUser() + { + return $this->user; + } + + /** + * Set the path to the private key file + * + * @param string $privateKey + * + * @return $this + */ + public function setPrivateKey($privateKey) + { + $this->privateKey = (string) $privateKey; + return $this; + } + + /** + * Get the path to the private key + * + * @return string + */ + public function getPrivateKey() + { + return $this->privateKey; + } + + /** + * Use a given resource to set the user and the key + * + * @param string + * + * @throws ConfigurationError + */ + public function setResource($resource = null) + { + $config = ResourceFactory::getResourceConfig($resource); + + if (! isset($config->user)) { + throw new ConfigurationError( + t("Can't send external Icinga Command. Remote user is missing") + ); + } + if (! isset($config->private_key)) { + throw new ConfigurationError( + t("Can't send external Icinga Command. The private key for the remote user is missing") + ); + } + + $this->setUser($config->user); + $this->setPrivateKey($config->private_key); + } + + /** + * Set the path to the Icinga command file on the remote host + * + * @param string $path + * + * @return $this + */ + public function setPath($path) + { + $this->path = (string) $path; + return $this; + } + + /** + * Get the path to the Icinga command file on the remote host + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Write the command to the Icinga command file on the remote host + * + * @param IcingaCommand $command + * @param int|null $now + * + * @throws ConfigurationError + * @throws CommandTransportException + */ + public function send(IcingaCommand $command, $now = null) + { + if (! isset($this->path)) { + throw new ConfigurationError( + 'Can\'t send external Icinga Command. Path to the remote command file is missing' + ); + } + if (! isset($this->host)) { + throw new ConfigurationError('Can\'t send external Icinga Command. Remote host is missing'); + } + $commandString = $this->renderer->render($command, $now); + Logger::debug( + 'Sending external Icinga command "%s" to the remote command file "%s:%u%s"', + $commandString, + $this->host, + $this->port, + $this->path + ); + return $this->sendCommandString($commandString); + } + + /** + * Get the SSH command + * + * @return string + */ + protected function sshCommand() + { + $cmd = sprintf( + 'exec ssh -o BatchMode=yes -p %u', + $this->port + ); + // -o BatchMode=yes for disabling interactive authentication methods + + if (isset($this->user)) { + $cmd .= ' -l ' . escapeshellarg($this->user); + } + + if (isset($this->privateKey)) { + // TODO: StrictHostKeyChecking=no for compat only, must be removed + $cmd .= ' -o StrictHostKeyChecking=no' + . ' -i ' . escapeshellarg($this->privateKey); + } + + $cmd .= sprintf( + ' %s "cat > %s"', + escapeshellarg($this->host), + escapeshellarg($this->path) + ); + + return $cmd; + } + + /** + * Send the command over SSH + * + * @param string $commandString + * + * @throws CommandTransportException + */ + protected function sendCommandString($commandString) + { + if ($this->isSshAlive()) { + $ret = fwrite($this->sshPipes[0], $commandString . "\n"); + if ($ret === false) { + $this->throwSshFailure('Cannot write to the remote command pipe'); + } elseif ($ret !== strlen($commandString) + 1) { + $this->throwSshFailure( + 'Failed to write the whole command to the remote command pipe' + ); + } + } else { + $this->throwSshFailure(); + } + } + + /** + * Get the pipes of the SSH subprocess + * + * @return array + */ + protected function getSshPipes() + { + if ($this->sshPipes === null) { + $this->forkSsh(); + } + + return $this->sshPipes; + } + + /** + * Get the SSH subprocess + * + * @return resource + */ + protected function getSshProcess() + { + if ($this->sshProcess === null) { + $this->forkSsh(); + } + + return $this->sshProcess; + } + + /** + * Get the status of the SSH subprocess + * + * @param string $what + * + * @return mixed + */ + protected function getSshProcessStatus($what = null) + { + $status = proc_get_status($this->getSshProcess()); + if ($what === null) { + return $status; + } else { + return $status[$what]; + } + } + + /** + * Get whether the SSH subprocess is alive + * + * @return bool + */ + protected function isSshAlive() + { + return $this->getSshProcessStatus('running'); + } + + /** + * Fork SSH subprocess + * + * @throws CommandTransportException If fork fails + */ + protected function forkSsh() + { + $descriptors = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + + $this->sshProcess = proc_open($this->sshCommand(), $descriptors, $this->sshPipes); + + if (! is_resource($this->sshProcess)) { + throw new CommandTransportException( + 'Can\'t send external Icinga command: Failed to fork SSH' + ); + } + } + + /** + * Read from STDERR + * + * @return string + */ + protected function readStderr() + { + return stream_get_contents($this->sshPipes[2]); + } + + /** + * Throw SSH failure + * + * @param string $msg + * + * @throws CommandTransportException + */ + protected function throwSshFailure($msg = 'Can\'t send external Icinga command') + { + throw new CommandTransportException( + '%s: %s', + $msg, + $this->readStderr() . var_export($this->getSshProcessStatus(), true) + ); + } + + /** + * Close SSH pipes and SSH subprocess + */ + public function __destruct() + { + if (is_resource($this->sshProcess)) { + fclose($this->sshPipes[0]); + fclose($this->sshPipes[1]); + fclose($this->sshPipes[2]); + + proc_close($this->sshProcess); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Controller.php b/modules/monitoring/library/Monitoring/Controller.php new file mode 100644 index 0000000..2628935 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Controller.php @@ -0,0 +1,159 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring; + +use ArrayIterator; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\QueryException; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use Icinga\File\Csv; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\Monitoring\Data\CustomvarProtectionIterator; +use Icinga\Util\Json; +use Icinga\Web\Controller as IcingaWebController; +use Icinga\Web\Url; + +/** + * Base class for all monitoring action controller + */ +class Controller extends IcingaWebController +{ + /** + * The backend used for this controller + * + * @var MonitoringBackend + */ + protected $backend; + + protected function moduleInit() + { + $this->backend = MonitoringBackend::instance($this->_getParam('backend')); + $this->view->url = Url::fromRequest(); + } + + protected function handleFormatRequest($query) + { + $desiredContentType = $this->getRequest()->getHeader('Accept'); + if ($desiredContentType === 'application/json') { + $desiredFormat = 'json'; + } elseif ($desiredContentType === 'text/csv') { + $desiredFormat = 'csv'; + } else { + $desiredFormat = strtolower($this->params->get('format', 'html')); + } + + if ($desiredFormat !== 'html' && ! $this->params->has('limit')) { + $query->limit(); // Resets any default limit and offset + } + + switch ($desiredFormat) { + case 'sql': + echo '<pre>' + . htmlspecialchars(wordwrap($query->dump())) + . '</pre>'; + exit; + case 'json': + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'application/json') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'inline; filename=' . $this->getRequest()->getActionName() . '.json' + ) + ->appendBody( + Json::sanitize( + iterator_to_array( + new CustomvarProtectionIterator( + new ArrayIterator($query->fetchAll()) + ) + ) + ) + ) + ->sendResponse(); + exit; + case 'csv': + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'text/csv') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv' + ) + ->appendBody((string) Csv::fromQuery(new CustomvarProtectionIterator($query))) + ->sendResponse(); + exit; + } + } + + /** + * Apply a restriction of the authenticated on the given filterable + * + * @param string $name Name of the restriction + * @param Filterable $filterable Filterable to restrict + * + * @return Filterable The filterable having the restriction applied + */ + protected function applyRestriction($name, Filterable $filterable) + { + $filterable->applyFilter($this->getRestriction($name)); + return $filterable; + } + + /** + * Get a restriction of the authenticated + * + * @param string $name Name of the restriction + * + * @return Filter Filter object + * @throws ConfigurationError If the restriction contains invalid filter columns + */ + protected function getRestriction($name) + { + $restriction = Filter::matchAny(); + $restriction->setAllowedFilterColumns(array( + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + function ($c) { + return preg_match('/^_(?:host|service)_/i', $c); + } + )); + foreach ($this->getRestrictions($name) as $filter) { + if ($filter === '*') { + return Filter::matchAll(); + } + try { + $restriction->addFilter(Filter::fromQueryString($filter)); + } catch (QueryException $e) { + throw new ConfigurationError( + $this->translate( + 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s' + ), + $name, + $filter, + implode(', ', array( + 'instance_name', + 'host_name', + 'hostgroup_name', + 'service_description', + 'servicegroup_name', + '_(host|service)_<customvar-name>' + )), + $e + ); + } + } + + if ($restriction->isEmpty()) { + return Filter::matchAll(); + } + + return $restriction; + } +} diff --git a/modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php b/modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php new file mode 100644 index 0000000..0ad051b --- /dev/null +++ b/modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php @@ -0,0 +1,30 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Data; + +use ArrayIterator; +use FilterIterator; +use Zend_Db_Expr; + +/** + * Iterator over non-pseudo monitoring query columns + */ +class ColumnFilterIterator extends FilterIterator +{ + /** + * Create a new ColumnFilterIterator + * + * @param array $columns + */ + public function __construct(array $columns) + { + parent::__construct(new ArrayIterator($columns)); + } + + public function accept(): bool + { + $column = $this->current(); + return ! ($column instanceof Zend_Db_Expr || $column === '(NULL)'); + } +} diff --git a/modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php b/modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php new file mode 100644 index 0000000..c3cc01a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php @@ -0,0 +1,25 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Data; + +use Icinga\Module\Monitoring\Object\MonitoredObject; +use IteratorIterator; + +class CustomvarProtectionIterator extends IteratorIterator +{ + const IS_CV_RE = '~^_(host|service)_([a-zA-Z0-9_]+)$~'; + + public function current(): object + { + $row = parent::current(); + + foreach ($row as $col => $val) { + if (preg_match(self::IS_CV_RE, $col, $m)) { + $row->$col = MonitoredObject::protectCustomVars([$m[2] => $val])[$m[2]]; + } + } + + return $row; + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Command.php b/modules/monitoring/library/Monitoring/DataView/Command.php new file mode 100644 index 0000000..6beb8bc --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Command.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * View representation for commands + */ +class Command extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'command_id', + 'command_instance_id', + 'command_config_type', + 'command_line', + 'command_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Comment.php b/modules/monitoring/library/Monitoring/DataView/Comment.php new file mode 100644 index 0000000..3a035bc --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Comment.php @@ -0,0 +1,82 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Host and service comments view + */ +class Comment extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'comment_author_name', + 'comment_data', + 'comment_expiration', + 'comment_internal_id', + 'comment_is_persistent', + 'comment_name', + 'comment_timestamp', + 'comment_type', + 'host_display_name', + 'host_name', + 'object_type', + 'service_description', + 'service_display_name', + 'service_host_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'comment_author', + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('host_display_name', 'service_display_name'); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'comment_timestamp' => array( + 'order' => self::SORT_DESC + ), + 'host_display_name' => array( + 'columns' => array( + 'host_display_name', + 'service_display_name' + ), + 'order' => self::SORT_ASC + ), + 'service_display_name' => array( + 'columns' => array( + 'service_display_name', + 'host_display_name' + ), + 'order' => self::SORT_ASC + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Commentevent.php b/modules/monitoring/library/Monitoring/DataView/Commentevent.php new file mode 100644 index 0000000..316700a --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Commentevent.php @@ -0,0 +1,30 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Commentevent extends DataView +{ + public function getColumns() + { + return array( + 'commentevent_id', + 'commentevent_entry_type', + 'commentevent_comment_time', + 'commentevent_author_name', + 'commentevent_comment_data', + 'commentevent_is_persistent', + 'commentevent_comment_source', + 'commentevent_expires', + 'commentevent_expiration_time', + 'commentevent_deletion_time', + 'host_name', + 'service_description' + ); + } + + public function getStaticFilterColumns() + { + return array('commentevent_id'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Contact.php b/modules/monitoring/library/Monitoring/DataView/Contact.php new file mode 100644 index 0000000..986acab --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Contact.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Contact extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'contact_object_id', + 'contact_id', + 'contact_name', + 'contact_alias', + 'contact_email', + 'contact_pager', + 'contact_has_host_notfications', + 'contact_has_service_notfications', + 'contact_can_submit_commands', + 'contact_notify_service_recovery', + 'contact_notify_service_warning', + 'contact_notify_service_critical', + 'contact_notify_service_unknown', + 'contact_notify_service_flapping', + 'contact_notify_service_downtime', + 'contact_notify_host_recovery', + 'contact_notify_host_down', + 'contact_notify_host_unreachable', + 'contact_notify_host_flapping', + 'contact_notify_host_downtime', + 'contact_notify_host_timeperiod', + 'contact_notify_service_timeperiod' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'contact_name' => array( + 'order' => self::SORT_ASC + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'contact', 'instance_name', + 'contactgroup', 'contactgroup_name', 'contactgroup_alias', + 'host', 'host_name', 'host_display_name', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('contact_alias'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Contactgroup.php b/modules/monitoring/library/Monitoring/DataView/Contactgroup.php new file mode 100644 index 0000000..84eecd1 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Contactgroup.php @@ -0,0 +1,57 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Contactgroup extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'contactgroup_name', + 'contactgroup_alias', + 'contact_count' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'contactgroup_name' => array( + 'order' => self::SORT_ASC + ), + 'contactgroup_alias' => array( + 'order' => self::SORT_ASC + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'contactgroup', + 'host', 'host_name', 'host_display_name', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('contactgroup_alias'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Customvar.php b/modules/monitoring/library/Monitoring/DataView/Customvar.php new file mode 100644 index 0000000..c02d52f --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Customvar.php @@ -0,0 +1,47 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Customvar extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'varname', + 'varvalue', + 'is_json', + 'host_name', + 'service_description', + 'contact_name', + 'object_type', + 'object_type_id' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'varname' => array( + 'columns' => array( + 'varname', + 'varvalue' + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array('host', 'service', 'contact'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php new file mode 100644 index 0000000..5b16e28 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/DataView.php @@ -0,0 +1,608 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +use Icinga\Data\Filter\FilterExpression; +use Icinga\Data\Filter\FilterMatch; +use IteratorAggregate; +use Icinga\Application\Hook; +use Icinga\Data\ConnectionInterface; +use Icinga\Data\Filter\Filter; +use Icinga\Data\FilterColumns; +use Icinga\Data\PivotTable; +use Icinga\Data\QueryInterface; +use Icinga\Data\SortRules; +use Icinga\Exception\QueryException; +use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Request; +use Icinga\Web\Url; +use Traversable; + +/** + * A read-only view of an underlying query + */ +abstract class DataView implements QueryInterface, SortRules, FilterColumns, IteratorAggregate +{ + /** + * The query used to populate the view + * + * @var IdoQuery + */ + protected $query; + + protected $connection; + + protected $isSorted = false; + + /** + * The cache for all filter columns + * + * @var array + */ + protected $filterColumns; + + /** + * Create a new view + * + * @param ConnectionInterface $connection + * @param array $columns + */ + public function __construct(ConnectionInterface $connection, array $columns = null) + { + $this->connection = $connection; + $this->query = $connection->query($this->getQueryName(), $columns); + } + + /** + * Return a iterator for all rows of the result set + * + * @return IdoQuery + */ + public function getIterator(): Traversable + { + return $this->getQuery(); + } + + /** + * Return the current position of the result set's iterator + * + * @return int + */ + public function getIteratorPosition() + { + return $this->query->getIteratorPosition(); + } + + /** + * Get the query name this data view relies on + * + * By default this is this class' name without its namespace + * + * @return string + */ + public static function getQueryName() + { + $tableName = explode('\\', get_called_class()); + $tableName = end($tableName); + return $tableName; + } + + public function where($condition, $value = null) + { + $this->query->where($condition, $value); + return $this; + } + + /** + * Add a filter expression, with as less validation as possible + * + * @param FilterExpression $ex + * + * @internal If you use this outside the monitoring module, it's your fault if something breaks + * @return $this + */ + public function whereEx(FilterExpression $ex) + { + $this->query->whereEx($ex); + return $this; + } + + public function dump() + { + if (! $this->isSorted) { + $this->order(); + } + return $this->query->dump(); + } + + /** + * Retrieve columns provided by this view + * + * @return array + */ + abstract public function getColumns(); + + protected function getHookedColumns() + { + $columns = array(); + foreach (Hook::all('monitoring/dataviewExtension') as $hook) { + foreach ($hook->getAdditionalQueryColumns($this->getQueryName()) as $col) { + $columns[] = $col; + } + } + + return $columns; + } + + /** + * Create view from params + * + * @param array $params + * @param array $columns + * + * @return static + */ + public static function fromParams(array $params, array $columns = null) + { + $view = new static(MonitoringBackend::instance($params['backend']), $columns); + + foreach ($params as $key => $value) { + if ($view->isValidFilterTarget($key)) { + $view->where($key, $value); + } + } + + if (isset($params['sort'])) { + $order = isset($params['order']) ? $params['order'] : null; + if ($order !== null) { + if (strtolower($order) === 'desc') { + $order = self::SORT_DESC; + } else { + $order = self::SORT_ASC; + } + } + + $view->order($params['sort'], $order); + } + return $view; + } + + /** + * Check whether the given column is a valid filter column + * + * @param string $column + * + * @return bool + */ + public function isValidFilterTarget($column) + { + // Customvar + if ($column[0] === '_' && preg_match('/^_(?:host|service)_/i', $column)) { + return true; + } + return in_array($column, $this->getColumns()) || in_array($column, $this->getStaticFilterColumns()); + } + + /** + * Return all filter columns with their optional label as key + * + * This will merge the results of self::getColumns(), self::getStaticFilterColumns() and + * self::getDynamicFilterColumns() *once*. (i.e. subsequent calls of this function will + * return the same result.) + * + * @return array + */ + public function getFilterColumns() + { + if ($this->filterColumns === null) { + $columns = array_merge( + $this->getColumns(), + $this->getStaticFilterColumns(), + $this->getDynamicFilterColumns() + ); + + $this->filterColumns = array(); + foreach ($columns as $label => $column) { + if (is_int($label)) { + $label = ucwords(str_replace('_', ' ', $column)); + } + + if ($this->query->isCaseInsensitive($column)) { + $label .= ' ' . t('(Case insensitive)'); + } + + $this->filterColumns[$label] = $column; + } + } + + return $this->filterColumns; + } + + /** + * Return all static filter columns + * + * @return array + */ + public function getStaticFilterColumns() + { + return array(); + } + + /** + * Return all dynamic filter columns such as custom variables + * + * @return array + */ + public function getDynamicFilterColumns() + { + $columns = array(); + if (! $this->query->allowsCustomVars()) { + return $columns; + } + + $query = MonitoringBackend::instance() + ->select() + ->from('customvar', array('varname', 'object_type')) + ->where('is_json', 0) + ->where('object_type_id', array(1, 2)) + ->getQuery()->group(array('varname', 'object_type')); + foreach ($query as $row) { + if ($row->object_type === 'host') { + $label = t('Host') . ' ' . ucwords(str_replace('_', ' ', $row->varname)); + $columns[$label] = '_host_' . $row->varname; + } else { // $row->object_type === 'service' + $label = t('Service') . ' ' . ucwords(str_replace('_', ' ', $row->varname)); + $columns[$label] = '_service_' . $row->varname; + } + } + + return $columns; + } + + /** + * Return the current filter + * + * @return Filter + */ + public function getFilter() + { + return $this->query->getFilter(); + } + + /** + * Return a pivot table for the given columns based on the current query + * + * @param string $xAxisColumn The column to use for the x axis + * @param string $yAxisColumn The column to use for the y axis + * @param Filter $xAxisFilter The filter to apply on a query for the x axis + * @param Filter $yAxisFilter The filter to apply on a query for the y axis + * + * @return PivotTable + */ + public function pivot($xAxisColumn, $yAxisColumn, Filter $xAxisFilter = null, Filter $yAxisFilter = null) + { + $pivot = new PivotTable($this->query, $xAxisColumn, $yAxisColumn); + return $pivot->setXAxisFilter($xAxisFilter)->setYAxisFilter($yAxisFilter); + } + + /** + * Sort result set either by the given column (and direction) or the sort defaults + * + * @param string $column + * @param string $direction + * + * @return $this + */ + public function order($column = null, $direction = null) + { + $sortRules = $this->getSortRules(); + if ($column === null) { + // Use first available sort rule as default + if (empty($sortRules)) { + return $this; + } + $sortColumns = reset($sortRules); + if (! isset($sortColumns['columns'])) { + $sortColumns['columns'] = array(key($sortRules)); + } + } else { + if (isset($sortRules[$column])) { + $sortColumns = $sortRules[$column]; + if (! isset($sortColumns['columns'])) { + $sortColumns['columns'] = array($column); + } + } else { + $sortColumns = array( + 'columns' => array($column), + 'order' => $direction + ); + }; + } + + $direction = $direction === null ? ($sortColumns['order'] ?? static::SORT_ASC) : $direction; + $direction = (strtoupper($direction) === static::SORT_ASC) ? 'ASC' : 'DESC'; + + foreach ($sortColumns['columns'] as $column) { + list($column, $order) = $this->query->splitOrder($column); + if (! $this->isValidFilterTarget($column)) { + throw new QueryException( + mt('monitoring', 'The sort column "%s" is not allowed in "%s".'), + $column, + get_class($this) + ); + } + $this->query->order($column, $order !== null ? $order : $direction); + } + $this->isSorted = true; + return $this; + } + + /** + * Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort + * + * @return array + */ + public function getSortRules() + { + return array(); + } + + /** + * Whether an order is set + * + * @return bool + */ + public function hasOrder() + { + return $this->query->hasOrder(); + } + + /** + * Get the order if any + * + * @return array|null + */ + public function getOrder() + { + return $this->query->getOrder(); + } + + public function getMappedField($field) + { + return $this->query->getMappedField($field); + } + + /** + * Return the query which was created in the constructor + * + * @return \Icinga\Data\SimpleQuery + */ + public function getQuery() + { + if (! $this->isSorted) { + $this->order(); + } + return $this->query; + } + + public function applyFilter(Filter $filter) + { + $this->validateFilterColumns($filter); + + return $this->addFilter($filter); + } + + /** + * Validates recursive the Filter columns against the isValidFilterTarget() method + * + * @param Filter $filter + * + * @throws \Icinga\Data\Filter\FilterException + */ + public function validateFilterColumns(Filter $filter) + { + if ($filter instanceof FilterMatch) { + if (! $this->isValidFilterTarget($filter->getColumn())) { + throw new QueryException( + mt('monitoring', 'The filter column "%s" is not allowed here.'), + $filter->getColumn() + ); + } + } + + if (method_exists($filter, 'filters')) { + foreach ($filter->filters() as $filter) { + $this->validateFilterColumns($filter); + } + } + } + + public function clearFilter() + { + $this->query->clearFilter(); + return $this; + } + + /** + * @deprecated(EL): Only use DataView::applyFilter() for applying filter because all other functions are missing + * column validation. Filter::matchAny() for the IdoQuery (or the DbQuery or the SimpleQuery I didn't have a look) + * is required for the filter to work properly. + */ + public function setFilter(Filter $filter) + { + $this->query->setFilter($filter); + return $this; + } + + /** + * Get the view's search columns + * + * @return string[] + */ + public function getSearchColumns() + { + return array(); + } + + /** + * @deprecated(EL): Only use DataView::applyFilter() for applying filter because all other functions are missing + * column validation. + */ + public function addFilter(Filter $filter) + { + $this->query->addFilter($filter); + return $this; + } + + /** + * Count result set + * + * @return int + */ + public function count(): int + { + return $this->query->count(); + } + + /** + * Set whether the query should peek ahead for more results + * + * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will + * be removed from the result set. Note that this only applies when fetching multiple results of limited queries. + * + * @return $this + */ + public function peekAhead($state = true) + { + $this->query->peekAhead($state); + return $this; + } + + /** + * Return whether the query did not yield all available results + * + * @return bool + */ + public function hasMore() + { + return $this->query->hasMore(); + } + + /** + * Return whether this query will or has yielded any result + * + * @return bool + */ + public function hasResult() + { + return $this->query->hasResult(); + } + + /** + * Set a limit count and offset + * + * @param int $count Number of rows to return + * @param int $offset Start returning after this many rows + * + * @return self + */ + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + return $this; + } + + /** + * Whether a limit is set + * + * @return bool + */ + public function hasLimit() + { + return $this->query->hasLimit(); + } + + /** + * Get the limit if any + * + * @return int|null + */ + public function getLimit() + { + return $this->query->getLimit(); + } + + /** + * Whether an offset is set + * + * @return bool + */ + public function hasOffset() + { + return $this->query->hasOffset(); + } + + /** + * Get the offset if any + * + * @return int|null + */ + public function getOffset() + { + return $this->query->getOffset(); + } + + /** + * Retrieve an array containing all rows of the result set + * + * @return array + */ + public function fetchAll() + { + return $this->getQuery()->fetchAll(); + } + + /** + * Fetch the first row of the result set + * + * @return mixed + */ + public function fetchRow() + { + return $this->getQuery()->fetchRow(); + } + + /** + * Fetch the first column of all rows of the result set as an array + * + * @return array + */ + public function fetchColumn() + { + return $this->getQuery()->fetchColumn(); + } + + /** + * Fetch the first column of the first row of the result set + * + * @return string + */ + public function fetchOne() + { + return $this->getQuery()->fetchOne(); + } + + /** + * Fetch all rows of the result set as an array of key-value pairs + * + * The first column is the key, the second column is the value. + * + * @return array + */ + public function fetchPairs() + { + return $this->getQuery()->fetchPairs(); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Downtime.php b/modules/monitoring/library/Monitoring/DataView/Downtime.php new file mode 100644 index 0000000..ca42e2d --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Downtime.php @@ -0,0 +1,96 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Host and service downtimes view + */ +class Downtime extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'downtime_author_name', + 'downtime_comment', + 'downtime_duration', + 'downtime_end', + 'downtime_entry_time', + 'downtime_internal_id', + 'downtime_is_fixed', + 'downtime_is_flexible', + 'downtime_is_in_effect', + 'downtime_name', + 'downtime_scheduled_end', + 'downtime_scheduled_start', + 'downtime_start', + 'host_display_name', + 'host_name', + 'host_state', + 'object_type', + 'service_description', + 'service_display_name', + 'service_host_name', + 'service_state' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'downtime_author', + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('host_display_name', 'service_display_name'); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'downtime_is_in_effect' => array( + 'columns' => array( + 'downtime_is_in_effect', + 'downtime_scheduled_start' + ), + 'order' => self::SORT_DESC + ), + 'downtime_start' => array( + 'order' => self::SORT_DESC + ), + 'host_display_name' => array( + 'columns' => array( + 'host_display_name', + 'service_display_name' + ), + 'order' => self::SORT_ASC + ), + 'service_display_name' => array( + 'columns' => array( + 'service_display_name', + 'host_display_name' + ), + 'order' => self::SORT_ASC + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Downtimeevent.php b/modules/monitoring/library/Monitoring/DataView/Downtimeevent.php new file mode 100644 index 0000000..a1fc0f6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Downtimeevent.php @@ -0,0 +1,33 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Downtimeevent extends DataView +{ + public function getColumns() + { + return array( + 'downtimeevent_id', + 'downtimeevent_entry_time', + 'downtimeevent_author_name', + 'downtimeevent_comment_data', + 'downtimeevent_is_fixed', + 'downtimeevent_scheduled_start_time', + 'downtimeevent_scheduled_end_time', + 'downtimeevent_was_started', + 'downtimeevent_actual_start_time', + 'downtimeevent_actual_end_time', + 'downtimeevent_was_cancelled', + 'downtimeevent_is_in_effect', + 'downtimeevent_trigger_time', + 'host_name', + 'service_description' + ); + } + + public function getStaticFilterColumns() + { + return array('downtimeevent_id'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Eventgrid.php b/modules/monitoring/library/Monitoring/DataView/Eventgrid.php new file mode 100644 index 0000000..1639e6b --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Eventgrid.php @@ -0,0 +1,60 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Eventgrid extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'day', + 'cnt_up', + 'cnt_down_hard', + 'cnt_down', + 'cnt_unreachable_hard', + 'cnt_unreachable', + 'cnt_unknown_hard', + 'cnt_unknown', + 'cnt_critical', + 'cnt_critical_hard', + 'cnt_warning', + 'cnt_warning_hard', + 'cnt_ok', + 'host_name', + 'host_display_name', + 'service_description', + 'service_display_name', + 'timestamp' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'day' => array( + 'order' => self::SORT_DESC + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_host_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php b/modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php new file mode 100644 index 0000000..9d9acc9 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php @@ -0,0 +1,7 @@ +<?php + +namespace Icinga\Module\Monitoring\DataView; + +class Eventgridhosts extends Eventgrid +{ +} diff --git a/modules/monitoring/library/Monitoring/DataView/Eventgridservices.php b/modules/monitoring/library/Monitoring/DataView/Eventgridservices.php new file mode 100644 index 0000000..faa1065 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Eventgridservices.php @@ -0,0 +1,7 @@ +<?php + +namespace Icinga\Module\Monitoring\DataView; + +class Eventgridservices extends Eventgrid +{ +} diff --git a/modules/monitoring/library/Monitoring/DataView/Eventhistory.php b/modules/monitoring/library/Monitoring/DataView/Eventhistory.php new file mode 100644 index 0000000..cd947f5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Eventhistory.php @@ -0,0 +1,60 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class EventHistory extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'id', + 'instance_name', + 'host_name', + 'host_display_name', + 'service_description', + 'service_display_name', + 'object_type', + 'timestamp', + 'state', + 'output', + 'type' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'timestamp' => array( + 'order' => self::SORT_DESC + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('host_display_name', 'service_display_name'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Flappingevent.php b/modules/monitoring/library/Monitoring/DataView/Flappingevent.php new file mode 100644 index 0000000..bc79497 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Flappingevent.php @@ -0,0 +1,27 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Flappingevent extends DataView +{ + public function getColumns() + { + return array( + 'flappingevent_id', + 'flappingevent_event_time', + 'flappingevent_event_type', + 'flappingevent_reason_type', + 'flappingevent_percent_state_change', + 'flappingevent_low_threshold', + 'flappingevent_high_threshold', + 'host_name', + 'service_description' + ); + } + + public function getStaticFilterColumns() + { + return array('flappingevent_id'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hostcomment.php b/modules/monitoring/library/Monitoring/DataView/Hostcomment.php new file mode 100644 index 0000000..74fc2ef --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hostcomment.php @@ -0,0 +1,45 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Host comment view + */ +class Hostcomment extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'comment_author', + 'comment_author_name', + 'comment_data', + 'comment_expiration', + 'comment_internal_id', + 'comment_is_persistent', + 'comment_name', + 'comment_timestamp', + 'comment_type', + 'host_display_name', + 'host_name', + 'object_type' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hostcontact.php b/modules/monitoring/library/Monitoring/DataView/Hostcontact.php new file mode 100644 index 0000000..ecfed2f --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hostcontact.php @@ -0,0 +1,17 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Hostcontact extends Contact +{ + public function getColumns() + { + return [ + 'contact_name', + 'contact_alias', + 'contact_email', + 'contact_pager' + ]; + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hostdowntime.php b/modules/monitoring/library/Monitoring/DataView/Hostdowntime.php new file mode 100644 index 0000000..f5e4e80 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hostdowntime.php @@ -0,0 +1,50 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Host downtime view + */ +class Hostdowntime extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'downtime_author', + 'downtime_author_name', + 'downtime_comment', + 'downtime_duration', + 'downtime_end', + 'downtime_entry_time', + 'downtime_internal_id', + 'downtime_is_fixed', + 'downtime_is_flexible', + 'downtime_is_in_effect', + 'downtime_name', + 'downtime_scheduled_end', + 'downtime_scheduled_start', + 'downtime_start', + 'host_display_name', + 'host_name', + 'object_type' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hostgroup.php b/modules/monitoring/library/Monitoring/DataView/Hostgroup.php new file mode 100644 index 0000000..b204fcd --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hostgroup.php @@ -0,0 +1,34 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Host group data view + */ +class Hostgroup extends DataView +{ + public function getColumns() + { + return array( + 'hostgroup_alias', + 'hostgroup_name' + ); + } + + public function getSortRules() + { + return array( + 'hostgroup_alias' => array( + 'order' => self::SORT_ASC + ) + ); + } + + public function getStaticFilterColumns() + { + return array( + 'instance_name', 'host_name', 'service_description', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php b/modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php new file mode 100644 index 0000000..9ed2eb9 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php @@ -0,0 +1,81 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Data view for the host group summary + */ +class Hostgroupsummary extends DataView +{ + public function getColumns() + { + return array( + 'hostgroup_alias', + 'hostgroup_name', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_pending', + 'hosts_severity', + 'hosts_total', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_up', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning_handled', + 'services_warning_unhandled' + ); + } + + public function getSearchColumns() + { + return array('hostgroup', 'hostgroup_alias'); + } + + public function getSortRules() + { + return array( + 'hostgroup_alias' => array( + 'order' => self::SORT_ASC + ), + 'hosts_severity' => array( + 'columns' => array( + 'hosts_severity', + 'hostgroup_alias ASC' + ), + 'order' => self::SORT_DESC + ) + ); + } + + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host_contact', 'host_contactgroup', 'host_name', + 'hostgroup', + 'service_description', + 'servicegroup_name' + ); + } + + public function getFilterColumns() + { + if ($this->filterColumns === null) { + $filterColumns = parent::getFilterColumns(); + $diff = array_diff($filterColumns, $this->getColumns()); + $this->filterColumns = array_merge($diff, [ + 'Hostgroup Name' => 'hostgroup_name', + 'Hostgroup Alias' => 'hostgroup_alias' + ]); + } + + return $this->filterColumns; + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hoststatus.php b/modules/monitoring/library/Monitoring/DataView/Hoststatus.php new file mode 100644 index 0000000..6440fe5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hoststatus.php @@ -0,0 +1,129 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class HostStatus extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array_merge($this->getHookedColumns(), array( + 'host_acknowledged', + 'host_acknowledgement_type', + 'host_action_url', + 'host_active_checks_enabled', + 'host_active_checks_enabled_changed', + 'host_address', + 'host_address6', + 'host_alias', + 'host_check_command', + 'host_check_execution_time', + 'host_check_latency', + 'host_check_source', + 'host_check_timeperiod', + 'host_current_check_attempt', + 'host_current_notification_number', + 'host_display_name', + 'host_event_handler_enabled', + 'host_event_handler_enabled_changed', + 'host_flap_detection_enabled', + 'host_flap_detection_enabled_changed', + 'host_handled', + 'host_hard_state', + 'host_in_downtime', + 'host_ipv4', + 'host_is_flapping', + 'host_is_reachable', + 'host_last_check', + 'host_last_notification', + 'host_last_state_change', + 'host_last_state_change_ts', + 'host_long_output', + 'host_max_check_attempts', + 'host_modified_host_attributes', + 'host_name', + 'host_next_check', + 'host_notes_url', + 'host_notifications_enabled', + 'host_notifications_enabled_changed', + 'host_obsessing', + 'host_obsessing_changed', + 'host_output', + 'host_passive_checks_enabled', + 'host_passive_checks_enabled_changed', + 'host_percent_state_change', + 'host_perfdata', + 'host_problem', + 'host_severity', + 'host_state', + 'host_state_type', + 'host_unhandled', + 'instance_name' + )); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', 'host_contact', 'host_contactgroup', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns($search = null) + { + if ($search !== null + && (@inet_pton($search) !== false || preg_match('/^\d{1,3}\.\d{1,3}\./', $search)) + ) { + return array('host', 'host_address', 'host_address6'); + } else { + if ($this->connection->isIcinga2()) { + return array('host', 'host_display_name'); + } else { + return array('host', 'host_display_name', 'host_alias'); + } + } + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'host_display_name' => array( + 'order' => self::SORT_ASC + ), + 'host_severity' => array( + 'columns' => array( + 'host_severity', + 'host_last_state_change_ts DESC' + ), + 'order' => self::SORT_DESC + ), + 'host_address' => array( + 'columns' => array( + 'host_ipv4' + ), + 'order' => self::SORT_ASC + ), + 'host_last_state_change' => array( + 'columns' => array( + 'host_last_state_change_ts' + ), + 'order' => self::SORT_DESC + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php b/modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php new file mode 100644 index 0000000..a857466 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Data view for host status summaries + */ +class Hoststatussummary extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_pending', + 'hosts_total', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_up', + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host', 'host_alias', 'host_display_name', 'host_name', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Instance.php b/modules/monitoring/library/Monitoring/DataView/Instance.php new file mode 100644 index 0000000..98ef1d6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Instance.php @@ -0,0 +1,33 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * View representation for instances + */ +class Instance extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'instance_id', + 'instance_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'instance_name' => array( + 'order' => self::SORT_ASC + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Notification.php b/modules/monitoring/library/Monitoring/DataView/Notification.php new file mode 100644 index 0000000..90755de --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Notification.php @@ -0,0 +1,59 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Notification extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'host_display_name', + 'host_name', + 'notification_contact_name', + 'notification_output', + 'notification_reason', + 'notification_state', + 'notification_timestamp', + 'object_type', + 'service_description', + 'service_display_name', + 'service_host_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'notification_timestamp' => array( + 'order' => self::SORT_DESC + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'hostgroup_name', + 'instance_name', + 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('host_display_name', 'service_display_name'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Notificationevent.php b/modules/monitoring/library/Monitoring/DataView/Notificationevent.php new file mode 100644 index 0000000..82dd212 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Notificationevent.php @@ -0,0 +1,29 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Notificationevent extends DataView +{ + public function getColumns() + { + return array( + 'notificationevent_id', + 'notificationevent_reason', + 'notificationevent_start_time', + 'notificationevent_end_time', + 'notificationevent_state', + 'notificationevent_output', + 'notificationevent_long_output', + 'notificationevent_escalated', + 'notificationevent_contacts_notified', + 'host_name', + 'service_description' + ); + } + + public function getStaticFilterColumns() + { + return array('notificationevent_id'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Programstatus.php b/modules/monitoring/library/Monitoring/DataView/Programstatus.php new file mode 100644 index 0000000..d611c72 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Programstatus.php @@ -0,0 +1,44 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * View for programstatus query + */ +class Programstatus extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'id', + 'status_update_time', + 'program_start_time', + 'program_end_time', + 'is_currently_running', + 'process_id', + 'daemon_mode', + 'last_command_check', + 'last_log_rotation', + 'notifications_enabled', + 'disable_notif_expire_time', + 'active_service_checks_enabled', + 'passive_service_checks_enabled', + 'active_host_checks_enabled', + 'passive_host_checks_enabled', + 'event_handlers_enabled', + 'flap_detection_enabled', + 'failure_prediction_enabled', + 'process_performance_data', + 'obsess_over_hosts', + 'obsess_over_services', + 'modified_host_attributes', + 'modified_service_attributes', + 'global_host_event_handler', + 'global_service_event_handler', + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Runtimesummary.php b/modules/monitoring/library/Monitoring/DataView/Runtimesummary.php new file mode 100644 index 0000000..bf80226 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Runtimesummary.php @@ -0,0 +1,38 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * View for runtimesummary query + */ +class Runtimesummary extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'check_type', + 'active_checks_enabled', + 'passive_checks_enabled', + 'execution_time', + 'latency', + 'object_count', + 'object_type' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'active_checks_enabled' => array( + 'order' => self::SORT_ASC + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Runtimevariables.php b/modules/monitoring/library/Monitoring/DataView/Runtimevariables.php new file mode 100644 index 0000000..b3624b7 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Runtimevariables.php @@ -0,0 +1,34 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * View for runtimevariables query + */ +class Runtimevariables extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'id', + 'varname', + 'varvalue' + ); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'id' => array( + 'order' => self::SORT_ASC + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicecomment.php b/modules/monitoring/library/Monitoring/DataView/Servicecomment.php new file mode 100644 index 0000000..78c1333 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicecomment.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Service comment view + */ +class Servicecomment extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'comment_author', + 'comment_author_name', + 'comment_data', + 'comment_expiration', + 'comment_internal_id', + 'comment_is_persistent', + 'comment_name', + 'comment_timestamp', + 'comment_type', + 'host_display_name', + 'host_name', + 'object_type', + 'service_description', + 'service_display_name', + 'service_host_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicecontact.php b/modules/monitoring/library/Monitoring/DataView/Servicecontact.php new file mode 100644 index 0000000..55c9950 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicecontact.php @@ -0,0 +1,8 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Servicecontact extends Hostcontact +{ +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicedowntime.php b/modules/monitoring/library/Monitoring/DataView/Servicedowntime.php new file mode 100644 index 0000000..43d895e --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicedowntime.php @@ -0,0 +1,50 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Servicedowntime extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'downtime_author', + 'downtime_author_name', + 'downtime_comment', + 'downtime_duration', + 'downtime_end', + 'downtime_entry_time', + 'downtime_internal_id', + 'downtime_is_fixed', + 'downtime_is_flexible', + 'downtime_is_in_effect', + 'downtime_name', + 'downtime_scheduled_end', + 'downtime_scheduled_start', + 'downtime_start', + 'host_display_name', + 'host_name', + 'object_type', + 'service_description', + 'service_display_name', + 'service_host_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', 'host_alias', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'instance_name', + 'service', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicegroup.php b/modules/monitoring/library/Monitoring/DataView/Servicegroup.php new file mode 100644 index 0000000..9909a68 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicegroup.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Servicegroup extends DataView +{ + public function getColumns() + { + return array( + 'servicegroup_alias', + 'servicegroup_name' + ); + } + + public function getSortRules() + { + return array( + 'servicegroup_alias' => array( + 'order' => self::SORT_ASC + ) + ); + } + + public function getStaticFilterColumns() + { + return array( + 'instance_name', 'host_name', 'hostgroup_name', 'service_description' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php b/modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php new file mode 100644 index 0000000..9dc3ee0 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php @@ -0,0 +1,75 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Data view for service group summaries + */ +class Servicegroupsummary extends DataView +{ + public function getColumns() + { + return array( + 'servicegroup_alias', + 'servicegroup_name', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_severity', + 'services_total', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning_handled', + 'services_warning_unhandled' + ); + } + + public function getSearchColumns() + { + return array('servicegroup', 'servicegroup_alias'); + } + + public function getSortRules() + { + return array( + 'servicegroup_alias' => array( + 'order' => self::SORT_ASC + ), + 'services_severity' => array( + 'columns' => array( + 'services_severity', + 'servicegroup_alias ASC' + ), + 'order' => self::SORT_DESC + ) + ); + } + + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'services_severity', + 'host_contact', 'host_contactgroup', 'host_name', + 'hostgroup_name', + 'service_contact', 'service_contactgroup', 'service_description', + 'servicegroup' + ); + } + + public function getFilterColumns() + { + if ($this->filterColumns === null) { + $filterColumns = parent::getFilterColumns(); + $diff = array_diff($filterColumns, $this->getColumns()); + $this->filterColumns = array_merge($diff, [ + 'Servicegroup Name' => 'servicegroup_name', + 'Servicegroup Alias' => 'servicegroup_alias' + ]); + } + + return $this->filterColumns; + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicestatus.php b/modules/monitoring/library/Monitoring/DataView/Servicestatus.php new file mode 100644 index 0000000..e80c6f0 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicestatus.php @@ -0,0 +1,180 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class ServiceStatus extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array_merge($this->getHookedColumns(), array( + 'host_acknowledged', + 'host_action_url', + 'host_active_checks_enabled', + 'host_address', + 'host_address6', + 'host_alias', + 'host_check_source', + 'host_display_name', + 'host_handled', + 'host_hard_state', + 'host_in_downtime', + 'host_ipv4', + 'host_is_flapping', + 'host_last_check', + 'host_last_hard_state', + 'host_last_hard_state_change', + 'host_last_state_change', + 'host_last_time_down', + 'host_last_time_unreachable', + 'host_last_time_up', + 'host_long_output', + 'host_modified_host_attributes', + 'host_name', + 'host_notes_url', + 'host_notifications_enabled', + 'host_output', + 'host_passive_checks_enabled', + 'host_perfdata', + 'host_problem', + 'host_severity', + 'host_state', + 'host_state_type', + 'host_unhandled_service_count', + 'instance_name', + 'service_acknowledged', + 'service_acknowledgement_type', + 'service_action_url', + 'service_active_checks_enabled', + 'service_active_checks_enabled_changed', + 'service_attempt', + 'service_check_command', + 'service_check_source', + 'service_check_timeperiod', + 'service_current_check_attempt', + 'service_current_notification_number', + 'service_description', + 'service_display_name', + 'service_event_handler_enabled', + 'service_event_handler_enabled_changed', + 'service_flap_detection_enabled', + 'service_flap_detection_enabled_changed', + 'service_handled', + 'service_hard_state', + 'service_host_name', + 'service_in_downtime', + 'service_is_flapping', + 'service_is_reachable', + 'service_last_check', + 'service_last_hard_state', + 'service_last_hard_state_change', + 'service_last_notification', + 'service_last_state_change', + 'service_last_state_change_ts', + 'service_last_time_critical', + 'service_last_time_ok', + 'service_last_time_unknown', + 'service_last_time_warning', + 'service_long_output', + 'service_max_check_attempts', + 'service_modified_service_attributes', + 'service_next_check', + 'service_notes', + 'service_notes_url', + 'service_notifications_enabled', + 'service_notifications_enabled_changed', + 'service_obsessing', + 'service_obsessing_changed', + 'service_output', + 'service_passive_checks_enabled', + 'service_passive_checks_enabled_changed', + 'service_perfdata', + 'service_problem', + 'service_severity', + 'service_state', + 'service_state_type', + 'service_unhandled' + )); + } + + /** + * {@inheritdoc} + */ + public function getSortRules() + { + return array( + 'service_display_name' => array( + 'order' => self::SORT_ASC + ), + 'service_severity' => array( + 'columns' => array( + 'service_severity', + 'service_last_state_change_ts DESC' + ), + 'order' => self::SORT_DESC + ), + 'service_last_state_change' => array( + 'columns' => array( + 'service_last_state_change_ts' + ), + 'order' => self::SORT_DESC + ), + 'host_severity' => array( + 'columns' => array( + 'host_severity', + 'host_last_state_change DESC', + 'host_display_name ASC', + 'service_display_name ASC' + ), + 'order' => self::SORT_DESC + ), + 'host_display_name' => array( + 'columns' => array( + 'host_display_name', + 'service_display_name' + ), + 'order' => self::SORT_ASC + ), + 'host_address' => array( + 'columns' => array( + 'host_ipv4', + 'service_display_name' + ), + 'order' => self::SORT_ASC + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'host', + 'host_contact', + 'host_contactgroup', + 'hostgroup', + 'hostgroup_alias', + 'hostgroup_name', + 'service', + 'service_contact', + 'service_contactgroup', + 'service_host', + 'servicegroup', + 'servicegroup_alias', + 'servicegroup_name' + ); + } + + /** + * {@inheritdoc} + */ + public function getSearchColumns() + { + return array('service', 'service_display_name'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php b/modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php new file mode 100644 index 0000000..abd3593 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php @@ -0,0 +1,45 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Data view for service status summaries + */ +class Servicestatussummary extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'services_critical', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_ok', + 'services_pending', + 'services_total', + 'services_unknown', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_warning', + 'services_warning_handled', + 'services_warning_unhandled' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host', 'host_alias', 'host_display_name', 'host_name', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Statechangeevent.php b/modules/monitoring/library/Monitoring/DataView/Statechangeevent.php new file mode 100644 index 0000000..0b01aff --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Statechangeevent.php @@ -0,0 +1,32 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class Statechangeevent extends DataView +{ + public function getColumns() + { + return array( + 'statechangeevent_id', + 'statechangeevent_state_time', + 'statechangeevent_state_change', + 'statechangeevent_state', + 'statechangeevent_state_type', + 'statechangeevent_current_check_attempt', + 'statechangeevent_max_check_attempts', + 'statechangeevent_last_state', + 'statechangeevent_last_hard_state', + 'statechangeevent_output', + 'statechangeevent_long_output', + 'statechangeevent_check_source', + 'host_name', + 'service_description' + ); + } + + public function getStaticFilterColumns() + { + return array('statechangeevent_id'); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Statussummary.php b/modules/monitoring/library/Monitoring/DataView/Statussummary.php new file mode 100644 index 0000000..36efccb --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Statussummary.php @@ -0,0 +1,111 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +class StatusSummary extends DataView +{ + /** + * {@inheritdoc} + */ + public function getColumns() + { + return array( + 'hosts_up', + 'hosts_up_not_checked', + 'hosts_pending', + 'hosts_pending_not_checked', + 'hosts_down', + 'hosts_down_handled', + 'hosts_down_unhandled', + 'hosts_down_passive', + 'hosts_down_not_checked', + 'hosts_unreachable', + 'hosts_unreachable_handled', + 'hosts_unreachable_unhandled', + 'hosts_unreachable_passive', + 'hosts_unreachable_not_checked', + 'hosts_active', + 'hosts_passive', + 'hosts_not_checked', + 'hosts_not_processing_event_handlers', + 'hosts_not_triggering_notifications', + 'hosts_without_flap_detection', + 'hosts_flapping', + 'services_ok', + 'services_ok_not_checked', + 'services_pending', + 'services_pending_not_checked', + 'services_warning', + 'services_warning_handled', + 'services_warning_unhandled', + 'services_warning_passive', + 'services_warning_not_checked', + 'services_critical', + 'services_critical_handled', + 'services_critical_unhandled', + 'services_critical_passive', + 'services_critical_not_checked', + 'services_unknown', + 'services_unknown_handled', + 'services_unknown_unhandled', + 'services_unknown_passive', + 'services_unknown_not_checked', + 'services_active', + 'services_passive', + 'services_not_checked', + 'services_not_processing_event_handlers', + 'services_not_triggering_notifications', + 'services_without_flap_detection', + 'services_flapping', + + + 'services_ok_on_ok_hosts', + 'services_ok_not_checked_on_ok_hosts', + 'services_pending_on_ok_hosts', + 'services_pending_not_checked_on_ok_hosts', + 'services_warning_handled_on_ok_hosts', + 'services_warning_unhandled_on_ok_hosts', + 'services_warning_passive_on_ok_hosts', + 'services_warning_not_checked_on_ok_hosts', + 'services_critical_handled_on_ok_hosts', + 'services_critical_unhandled_on_ok_hosts', + 'services_critical_passive_on_ok_hosts', + 'services_critical_not_checked_on_ok_hosts', + 'services_unknown_handled_on_ok_hosts', + 'services_unknown_unhandled_on_ok_hosts', + 'services_unknown_passive_on_ok_hosts', + 'services_unknown_not_checked_on_ok_hosts', + 'services_ok_on_problem_hosts', + 'services_ok_not_checked_on_problem_hosts', + 'services_pending_on_problem_hosts', + 'services_pending_not_checked_on_problem_hosts', + 'services_warning_handled_on_problem_hosts', + 'services_warning_unhandled_on_problem_hosts', + 'services_warning_passive_on_problem_hosts', + 'services_warning_not_checked_on_problem_hosts', + 'services_critical_handled_on_problem_hosts', + 'services_critical_unhandled_on_problem_hosts', + 'services_critical_passive_on_problem_hosts', + 'services_critical_not_checked_on_problem_hosts', + 'services_unknown_handled_on_problem_hosts', + 'services_unknown_unhandled_on_problem_hosts', + 'services_unknown_passive_on_problem_hosts', + 'services_unknown_not_checked_on_problem_hosts' + ); + } + + /** + * {@inheritdoc} + */ + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host', 'host_alias', 'host_display_name', 'host_name', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php b/modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php new file mode 100644 index 0000000..4f5f392 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php @@ -0,0 +1,28 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Data view for unhandled host problems + */ +class Unhandledhostproblems extends DataView +{ + public function getColumns() + { + return array( + 'hosts_down_unhandled' + ); + } + + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host', 'host_alias', 'host_display_name', 'host_name', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php b/modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php new file mode 100644 index 0000000..3af4502 --- /dev/null +++ b/modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php @@ -0,0 +1,28 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\DataView; + +/** + * Data view for unhandled service problems + */ +class Unhandledserviceproblems extends DataView +{ + public function getColumns() + { + return array( + 'services_critical_unhandled' + ); + } + + public function getStaticFilterColumns() + { + return array( + 'instance_name', + 'host', 'host_alias', 'host_display_name', 'host_name', + 'hostgroup', 'hostgroup_alias', 'hostgroup_name', + 'service', 'service_description', 'service_display_name', + 'servicegroup', 'servicegroup_alias', 'servicegroup_name' + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Exception/CommandTransportException.php b/modules/monitoring/library/Monitoring/Exception/CommandTransportException.php new file mode 100644 index 0000000..5c08351 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Exception/CommandTransportException.php @@ -0,0 +1,13 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Exception; + +use Icinga\Exception\IcingaException; + +/** + * Exception thrown if a command was not sent + */ +class CommandTransportException extends IcingaException +{ +} diff --git a/modules/monitoring/library/Monitoring/Exception/CurlException.php b/modules/monitoring/library/Monitoring/Exception/CurlException.php new file mode 100644 index 0000000..01757af --- /dev/null +++ b/modules/monitoring/library/Monitoring/Exception/CurlException.php @@ -0,0 +1,13 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Exception; + +use Icinga\Exception\IcingaException; + +/** + * Exception thrown if {@link curl_exec()} fails + */ +class CurlException extends IcingaException +{ +} diff --git a/modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php b/modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php new file mode 100644 index 0000000..94d1af2 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + + +namespace Icinga\Module\Monitoring\Exception; + +use Icinga\Exception\IcingaException; + +class UnsupportedBackendException extends IcingaException +{ +} diff --git a/modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php b/modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php new file mode 100644 index 0000000..700bfd5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php @@ -0,0 +1,98 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Closure; +use Exception; +use Icinga\Application\Hook; +use Icinga\Application\Logger; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +abstract class CustomVarRendererHook +{ + /** + * Prefetch the data the hook needs to render custom variables + * + * @param MonitoredObject $object The object for which they'll be rendered + * + * @return bool Return true if the hook can render variables for the given object, false otherwise + */ + abstract public function prefetchForObject(MonitoredObject $object); + + /** + * Render the given variable name + * + * @param string $key + * + * @return ?mixed + */ + abstract public function renderCustomVarKey($key); + + /** + * Render the given variable value + * + * @param string $key + * @param mixed $value + * + * @return ?mixed + */ + abstract public function renderCustomVarValue($key, $value); + + /** + * Return a group name for the given variable name + * + * @param string $key + * + * @return ?string + */ + abstract public function identifyCustomVarGroup($key); + + /** + * Prepare available hooks to render custom variables of the given object + * + * @param MonitoredObject $object + * + * @return Closure A callback ($key, $value) which returns an array [$newKey, $newValue, $group] + */ + final public static function prepareForObject(MonitoredObject $object) + { + $hooks = []; + foreach (Hook::all('Monitoring/CustomVarRenderer') as $hook) { + /** @var self $hook */ + try { + if ($hook->prefetchForObject($object)) { + $hooks[] = $hook; + } + } catch (Exception $e) { + Logger::error('Failed to load hook %s:', get_class($hook), $e); + } + } + + return function ($key, $value) use ($hooks, $object) { + $newKey = $key; + $newValue = $value; + $group = null; + foreach ($hooks as $hook) { + /** @var self $hook */ + + try { + $renderedKey = $hook->renderCustomVarKey($key); + $renderedValue = $hook->renderCustomVarValue($key, $value); + $group = $hook->identifyCustomVarGroup($key); + } catch (Exception $e) { + Logger::error('Failed to use hook %s:', get_class($hook), $e); + continue; + } + + if ($renderedKey !== null || $renderedValue !== null) { + $newKey = $renderedKey !== null ? $renderedKey : $key; + $newValue = $renderedValue !== null ? $renderedValue : $value; + break; + } + } + + return [$newKey, $newValue, $group]; + }; + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php new file mode 100644 index 0000000..24b97c5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php @@ -0,0 +1,20 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +abstract class DataviewExtensionHook +{ + public function getAdditionalQueryColumns($queryName) + { + $cols = $this->provideAdditionalQueryColumns($queryName); + + if (! is_array($cols)) { + return array(); + } + + return $cols; + } + + abstract public function provideAdditionalQueryColumns($queryName); +} diff --git a/modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php new file mode 100644 index 0000000..9eb5ca3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php @@ -0,0 +1,126 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Application\ClassLoader; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use Icinga\Module\Monitoring\Object\ObjectList; +use Icinga\Web\View; + +/** + * Base class for hooks extending the detail view of monitored objects + * + * Extend this class if you want to extend the detail view of monitored objects with custom HTML. + */ +abstract class DetailviewExtensionHook +{ + /** + * The view the generated HTML will be included in + * + * @var View + */ + private $view; + + /** + * The module of the derived class + * + * @var Module + */ + private $module; + + /** + * Create a new hook + * + * @see init() For hook initialization. + */ + final public function __construct() + { + $this->init(); + } + + /** + * Overwrite this function for hook initialization, e.g. loading the hook's config + */ + protected function init() + { + } + + /** + * Shall return valid HTML to include in the detail view + * + * @param MonitoredObject $object The object to generate HTML for + * + * @return string + */ + abstract public function getHtmlForObject(MonitoredObject $object); + + /** + * Shall return valid HTML to include in the detail view of a multi-select view + * + * @param ObjectList $objects A list of objects shown in the multi-select view + * + * @return string + */ + public function getHtmlForObjects($objects) + { + // For compatibility empty by default + return ''; + } + + /** + * Get {@link view} + * + * @return View + */ + public function getView() + { + return $this->view; + } + + /** + * Set {@link view} + * + * @param View $view + * + * @return $this + */ + public function setView($view) + { + $this->view = $view; + return $this; + } + + /** + * Get the module of the derived class + * + * @return Module + */ + public function getModule() + { + if ($this->module === null) { + $class = get_class($this); + if (ClassLoader::classBelongsToModule($class)) { + $this->module = Icinga::app()->getModuleManager()->getModule(ClassLoader::extractModuleName($class)); + } + } + + return $this->module; + } + + /** + * Set the module of the derived class + * + * @param Module $module + * + * @return $this + */ + public function setModule(Module $module) + { + $this->module = $module; + + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php new file mode 100644 index 0000000..e0375d5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php @@ -0,0 +1,79 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Application\ClassLoader; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; + +/** + * Base class for hooks extending the event view of monitored objects + * + * Extend this class if you want to extend the event view of monitored objects with custom HTML. + */ +abstract class EventDetailsExtensionHook +{ + /** + * The module of the derived class + * + * @var Module + */ + private $module; + + /** + * Create a new hook + * + * @see init() For hook initialization. + */ + final public function __construct() + { + $this->init(); + } + /** + * Overwrite this function for hook initialization, e.g. loading the hook's config + */ + protected function init() + { + } + + + /** + * Shall return valid HTML to include in the detail view + * + * @param object $event The object to generate HTML for + * + * @return string + */ + abstract public function getHtmlForEvent($event); + + /** + * Get the module of the derived class + * + * @return Module + * @throws \Icinga\Exception\ProgrammingError + */ + public function getModule() + { + if ($this->module === null) { + $class = get_class($this); + if (ClassLoader::classBelongsToModule($class)) { + $this->module = Icinga::app()->getModuleManager()->getModule(ClassLoader::extractModuleName($class)); + } + } + return $this->module; + } + + /** + * Set the module of the derived class + * + * @param Module $module + * + * @return $this + */ + public function setModule(Module $module) + { + $this->module = $module; + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/HostActionsHook.php b/modules/monitoring/library/Monitoring/Hook/HostActionsHook.php new file mode 100644 index 0000000..def0090 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/HostActionsHook.php @@ -0,0 +1,52 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Module\Monitoring\Object\Host; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +/** + * Base class for host action hooks + */ +abstract class HostActionsHook extends ObjectActionsHook +{ + /** + * Implementors of this method should return an array containing + * additional action links for a specific host. You get a full Host + * object, which allows you to return specific links only for nodes + * with specific properties. + * + * The result array should be in the form title => url, where title will + * be used as link caption. Url should be an Icinga\Web\Url object when + * the link should point to an Icinga Web url - otherwise a string would + * be fine. + * + * Mixed example: + * <code> + * return array( + * 'Wiki' => 'http://my.wiki/host=' . rawurlencode($host->host_name), + * 'Logstash' => Url::fromPath( + * 'logstash/search/syslog', + * array('host' => $host->host_name) + * ) + * ); + * </code> + * + * One might also provide ssh:// or rdp:// urls if equipped with fitting + * (safe) URL handlers for his browser(s). + * + * TODO: I'd love to see some kind of a Link/LinkSet object implemented + * for this and similar hooks. + * + * @param Host $host Monitoring host object + * + * @return array An array containing a list of host action links + */ + abstract public function getActionsForHost(Host $host); + + public function getActionsForObject(MonitoredObject $object) + { + return $this->getActionsForHost($object); + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php new file mode 100644 index 0000000..64ac65c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php @@ -0,0 +1,15 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery; + +abstract class IdoQueryExtensionHook +{ + abstract public function extendColumnMap(IdoQuery $query); + + public function joinVirtualTable(IdoQuery $query, $virtualTable) + { + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php b/modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php new file mode 100644 index 0000000..eb2d910 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php @@ -0,0 +1,47 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Web\Navigation\Navigation; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +/** + * Base class for object action hooks + */ +abstract class ObjectActionsHook +{ + /** + * Return the action navigation for the given object + * + * @return Navigation + */ + public function getNavigation(MonitoredObject $object) + { + $urls = $this->getActionsForObject($object); + if (is_array($urls)) { + $navigation = new Navigation(); + foreach ($urls as $label => $url) { + $navigation->addItem($label, array('url' => $url)); + } + } else { + $navigation = $urls; + } + + return $navigation; + } + + /** + * Create and return a new Navigation object + * + * @param array $actions Optional array of actions to add to the returned object + * + * @return Navigation + */ + protected function createNavigation(array $actions = null) + { + return empty($actions) ? new Navigation() : Navigation::fromArray($actions); + } + + abstract public function getActionsForObject(MonitoredObject $object); +} diff --git a/modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php b/modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php new file mode 100644 index 0000000..15fa9bb --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php @@ -0,0 +1,60 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Authentication\Auth; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use Icinga\Web\Request; + +/** + * Base class for object host details custom tab hooks + */ +abstract class ObjectDetailsTabHook +{ + /** + * Return the tab name - it must be unique + * + * @return string + */ + abstract public function getName(); + + /** + * Return the tab label + * + * @return string + */ + abstract public function getLabel(); + + /** + * Return the tab header + * + * @param MonitoredObject $monitoredObject The monitored object related to that page + * @param Request $request + * @return string/bool The HTML string that compose the tab header, + * bool True if the default header should be shown, False to display nothing + */ + public function getHeader(MonitoredObject $monitoredObject, Request $request) + { + return true; + } + + /** + * Return the tab content + * + * @param MonitoredObject $monitoredObject The monitored object related to that page + * @param Request $request + * @return string The HTML string that compose the tab content + */ + abstract public function getContent(MonitoredObject $monitoredObject, Request $request); + + /** + * This method returns true if the tab is visible for the logged user, otherwise false + * + * @return bool True if the tab is visible for the logged user, otherwise false + */ + public function shouldBeShown(MonitoredObject $monitoredObject, Auth $auth) + { + return true; + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php b/modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php new file mode 100644 index 0000000..52ecd09 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php @@ -0,0 +1,46 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +/** + * Base class for plugin output hooks + * + * The Plugin Output Hook allows you to rewrite the plugin output based on check commands. + * You have to implement the following methods: + * * {@link getCommands()} + * * and {@link render()} + */ +abstract class PluginOutputHook +{ + /** + * Get the command or list of commands the hook is responsible for + * + * With this method you specify for which commands the provided hook is responsible for. You may return a single + * command as string or a list of commands as array. + * If you want your hook to be responsible for every command, you have to return the asterisk `'*'`. + * + * @return string|array + */ + abstract public function getCommands(); + + /** + * Render the given plugin output based on the specified check command + * + * With this method you rewrite the plugin output based on check commands. The parameter `$command` specifies the + * check command of the host or service and `$output` specifies the plugin output. The parameter `$detail` tells you + * whether the output is requested from the detail area of the host or service. + * + * Do not use complex logic for rewriting plugin output in list views because of the performance impact! + * + * You have to return the rewritten plugin output as string. It is also possible to return a HTML string here. + * Please refer to {@link \Icinga\Module\Monitoring\Web\Helper\PluginOutputPurifier} for a list of allowed tags. + * + * @param string $command Check command + * @param string $output Plugin output + * @param bool $detail Whether the output is requested from the detail area + * + * @return string Rewritten plugin output + */ + abstract public function render($command, $output, $detail); +} diff --git a/modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php b/modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php new file mode 100644 index 0000000..c6cf5f5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php @@ -0,0 +1,52 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Module\Monitoring\Object\MonitoredObject; + +/** + * Base class for host action hooks + */ +abstract class ServiceActionsHook extends ObjectActionsHook +{ + /** + * Implementors of this method should return an array containing + * additional action links for a specific host. You get a full Service + * object, which allows you to return specific links only for nodes + * with specific properties. + * + * The result array should be in the form title => url, where title will + * be used as link caption. Url should be an Icinga\Web\Url object when + * the link should point to an Icinga Web url - otherwise a string would + * be fine. + * + * Mixed example: + * <code> + * return array( + * 'Wiki' => 'http://my.wiki/host=' . rawurlencode($service->service_name), + * 'Logstash' => Url::fromPath( + * 'logstash/search/syslog', + * array('service' => $service->host_name) + * ) + * ); + * </code> + * + * One might also provide ssh:// or rdp:// urls if equipped with fitting + * (safe) URL handlers for his browser(s). + * + * TODO: I'd love to see some kind of a Link/LinkSet object implemented + * for this and similar hooks. + * + * @param Service $service Monitoring service object + * + * @return array An array containing a list of service action links + */ + abstract public function getActionsForService(Service $service); + + public function getActionsForObject(MonitoredObject $object) + { + return $this->getActionsForService($object); + } +} diff --git a/modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php b/modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php new file mode 100644 index 0000000..d302d12 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php @@ -0,0 +1,37 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Hook; + +use Icinga\Module\Monitoring\Timeline\TimeRange; + +/** + * Base class for TimeLine providers + */ +abstract class TimelineProviderHook +{ + /** + * Return the names by which to group entries + * + * @return array An array with the names as keys and their attribute-lists as values + */ + abstract public function getIdentifiers(); + + /** + * Return the visible entries supposed to be shown on the timeline + * + * @param TimeRange $range The range of time for which to fetch entries + * + * @return array The entries to display on the timeline + */ + abstract public function fetchEntries(TimeRange $range); + + /** + * Return the entries supposed to be used to calculate forecasts + * + * @param TimeRange $range The range of time for which to fetch forecasts + * + * @return array The entries to calculate forecasts with + */ + abstract public function fetchForecasts(TimeRange $range); +} diff --git a/modules/monitoring/library/Monitoring/MonitoringWizard.php b/modules/monitoring/library/Monitoring/MonitoringWizard.php new file mode 100644 index 0000000..51ead8a --- /dev/null +++ b/modules/monitoring/library/Monitoring/MonitoringWizard.php @@ -0,0 +1,159 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring; + +use Icinga\Web\Form; +use Icinga\Web\Wizard; +use Icinga\Web\Request; +use Icinga\Module\Setup\Setup; +use Icinga\Module\Setup\SetupWizard; +use Icinga\Module\Setup\RequirementSet; +use Icinga\Module\Setup\Forms\SummaryPage; +use Icinga\Module\Monitoring\Forms\Setup\WelcomePage; +use Icinga\Module\Monitoring\Forms\Setup\SecurityPage; +use Icinga\Module\Monitoring\Forms\Setup\TransportPage; +use Icinga\Module\Monitoring\Forms\Setup\IdoResourcePage; +use Icinga\Module\Setup\Requirement\PhpModuleRequirement; + +/** + * Monitoring Module Setup Wizard + */ +class MonitoringWizard extends Wizard implements SetupWizard +{ + /** + * Register all pages for this wizard + */ + public function init() + { + $this->addPage(new WelcomePage()); + $this->addPage(new IdoResourcePage()); + $this->addPage(new TransportPage()); + $this->addPage(new SecurityPage()); + $this->addPage(new SummaryPage(array('name' => 'setup_monitoring_summary'))); + } + + /** + * Setup the given page that is either going to be displayed or validated + * + * @param Form $page The page to setup + * @param Request $request The current request + */ + public function setupPage(Form $page, Request $request) + { + if ($page->getName() === 'setup_requirements') { + $page->setRequirements($this->getRequirements()); + } elseif ($page->getName() === 'setup_monitoring_summary') { + $page->setSummary($this->getSetup()->getSummary()); + $page->setSubjectTitle(mt('monitoring', 'the monitoring module', 'setup.summary.subject')); + } elseif ($this->getDirection() === static::FORWARD + && ($page->getName() === 'setup_monitoring_ido') + ) { + if ((($authDbResourceData = $this->getPageData('setup_auth_db_resource')) !== null + && $authDbResourceData['name'] === $request->getPost('name')) + || (($configDbResourceData = $this->getPageData('setup_config_db_resource')) !== null + && $configDbResourceData['name'] === $request->getPost('name')) + || (($ldapResourceData = $this->getPageData('setup_ldap_resource')) !== null + && $ldapResourceData['name'] === $request->getPost('name')) + ) { + $page->error(mt('monitoring', 'The given resource name is already in use.')); + } + } + } + + /** + * Add buttons to the given page based on its position in the page-chain + * + * @param Form $page The page to add the buttons to + * + * @todo This is never called, because its a sub-wizard only + * @todo This is missing the ´transport_validation´ case + * @see WebWizard::addButtons which does some of the needed work + */ + protected function addButtons(Form $page) + { + parent::addButtons($page); + + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + // Used t() here as "Start" is too generic and already translated in the icinga domain + $page->getElement(static::BTN_NEXT)->setLabel(t('Start', 'setup.welcome.btn.next')); + } elseif ($index === count($pages) - 1) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('monitoring', 'Setup the monitoring module for Icinga Web 2', 'setup.summary.btn.finish') + ); + } + + if ($page->getName() === 'setup_monitoring_ido') { + $page->addElement( + 'submit', + 'backend_validation', + array( + 'ignore' => true, + 'label' => t('Validate Configuration'), + 'data-progress-label' => t('Validation In Progress'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->getDisplayGroup('buttons')->addElement($page->getElement('backend_validation')); + } + } + + /** + * Return the setup for this wizard + * + * @return Setup + */ + public function getSetup() + { + $pageData = $this->getPageData(); + $setup = new Setup(); + + $setup->addStep( + new BackendStep(array( + 'backendConfig' => ['name' => 'icinga', 'type' => 'ido'], + 'resourceConfig' => array_diff_key( + $pageData['setup_monitoring_ido'], //TODO: Prefer a new backend once implemented. + array('skip_validation' => null) + ) + )) + ); + + $setup->addStep( + new TransportStep(array( + 'transportConfig' => $pageData['setup_command_transport'] + )) + ); + + $setup->addStep( + new SecurityStep(array( + 'securityConfig' => $pageData['setup_monitoring_security'] + )) + ); + + return $setup; + } + + /** + * Return the requirements of this wizard + * + * @return RequirementSet + */ + public function getRequirements() + { + $set = new RequirementSet(); + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'curl', + 'alias' => 'cURL', + 'description' => mt( + 'monitoring', + 'To send external commands over Icinga 2\'s API the cURL module for PHP is required.' + ) + ))); + + return $set; + } +} diff --git a/modules/monitoring/library/Monitoring/Object/Acknowledgement.php b/modules/monitoring/library/Monitoring/Object/Acknowledgement.php new file mode 100644 index 0000000..3cd0d20 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/Acknowledgement.php @@ -0,0 +1,215 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use InvalidArgumentException; +use Traversable; +use Icinga\Util\StringHelper; + +/** + * Acknowledgement of a host or service incident + */ +class Acknowledgement +{ + /** + * Author of the acknowledgement + * + * @var string + */ + protected $author; + + /** + * Comment of the acknowledgement + * + * @var string + */ + protected $comment; + + /** + * Entry time of the acknowledgement + * + * @var int + */ + protected $entryTime; + + /** + * Expiration time of the acknowledgment + * + * @var int|null + */ + protected $expirationTime; + + /** + * Whether the acknowledgement is sticky + * + * Sticky acknowledgements suppress notifications until the host or service recovers + * + * @var bool + */ + protected $sticky = false; + + /** + * Create a new acknowledgement of a host or service incident + * + * @param array|object|Traversable $properties + * + * @throws InvalidArgumentException If the type of the given properties is invalid + */ + public function __construct($properties = null) + { + if ($properties !== null) { + $this->setProperties($properties); + } + } + + /** + * Get the author of the acknowledgement + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Set the author of the acknowledgement + * + * @param string $author + * + * @return $this + */ + public function setAuthor($author) + { + $this->author = (string) $author; + return $this; + } + + /** + * Get the comment of the acknowledgement + * + * @return string + */ + public function getComment() + { + return $this->comment; + } + + /** + * Set the comment of the acknowledgement + * + * @param string $comment + * + * @return $this + */ + public function setComment($comment) + { + $this->comment = (string) $comment; + + return $this; + } + + /** + * Get the entry time of the acknowledgement + * + * @return int + */ + public function getEntryTime() + { + return $this->entryTime; + } + + /** + * Set the entry time of the acknowledgement + * + * @param int $entryTime + * + * @return $this + */ + public function setEntryTime($entryTime) + { + $this->entryTime = (int) $entryTime; + + return $this; + } + + /** + * Get the expiration time of the acknowledgement + * + * @return int|null + */ + public function getExpirationTime() + { + return $this->expirationTime; + } + + /** + * Set the expiration time of the acknowledgement + * + * @param int|null $expirationTime Unix timestamp + * + * @return $this + */ + public function setExpirationTime($expirationTime = null) + { + $this->expirationTime = $expirationTime !== null ? (int) $expirationTime : null; + + return $this; + } + + /** + * Get whether the acknowledgement is sticky + * + * @return bool + */ + public function getSticky() + { + return $this->sticky; + } + + /** + * Set whether the acknowledgement is sticky + * + * @param bool $sticky + * + * @return $this + */ + public function setSticky($sticky = true) + { + $this->sticky = (bool) $sticky; + return $this; + } + + /** + * Get whether the acknowledgement expires + * + * @return bool + */ + public function expires() + { + return $this->expirationTime !== null; + } + + /** + * Set the properties of the acknowledgement + * + * @param array|object|Traversable $properties + * + * @return $this + * @throws InvalidArgumentException If the type of the given properties is invalid + */ + public function setProperties($properties) + { + if (! is_array($properties) && ! is_object($properties) && ! $properties instanceof Traversable) { + throw new InvalidArgumentException('Properties must be either an array or an instance of Traversable'); + } + foreach ($properties as $name => $value) { + $setter = 'set' . ucfirst(StringHelper::cname($name)); + if (method_exists($this, $setter)) { + $this->$setter($value); + } + } + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Object/Host.php b/modules/monitoring/library/Monitoring/Object/Host.php new file mode 100644 index 0000000..dfb25ed --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/Host.php @@ -0,0 +1,204 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use Icinga\Data\Filter\FilterEqual; +use InvalidArgumentException; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; + +/** + * An Icinga host + */ +class Host extends MonitoredObject +{ + /** + * Host state 'UP' + */ + const STATE_UP = 0; + + /** + * Host state 'DOWN' + */ + const STATE_DOWN = 1; + + /** + * Host state 'UNREACHABLE' + */ + const STATE_UNREACHABLE = 2; + + /** + * Host state 'PENDING' + */ + const STATE_PENDING = 99; + + /** + * Type of the Icinga host + * + * @var string + */ + public $type = self::TYPE_HOST; + + /** + * Prefix of the Icinga host + * + * @var string + */ + public $prefix = 'host_'; + + /** + * Hostname + * + * @var string + */ + protected $host; + + /** + * The services running on the hosts + * + * @var \Icinga\Module\Monitoring\Object\Service[] + */ + protected $services; + + /** + * Create a new host + * + * @param MonitoringBackend $backend Backend to fetch host information from + * @param string $host Hostname + */ + public function __construct(MonitoringBackend $backend, $host) + { + parent::__construct($backend); + $this->host = $host; + } + + /** + * Get the hostname + * + * @return string + */ + public function getName() + { + return $this->host; + } + + /** + * Get the data view to fetch the host information from + * + * @return \Icinga\Module\Monitoring\DataView\HostStatus + */ + protected function getDataView() + { + $columns = array( + 'host_acknowledged', + 'host_acknowledgement_type', + 'host_action_url', + 'host_active_checks_enabled', + 'host_active_checks_enabled_changed', + 'host_address', + 'host_address6', + 'host_alias', + 'host_attempt', + 'host_check_command', + 'host_check_execution_time', + 'host_check_interval', + 'host_check_latency', + 'host_check_source', + 'host_check_timeperiod', + 'host_current_check_attempt', + 'host_current_notification_number', + 'host_display_name', + 'host_event_handler_enabled', + 'host_event_handler_enabled_changed', + 'host_flap_detection_enabled', + 'host_flap_detection_enabled_changed', + 'host_handled', + 'host_icon_image', + 'host_icon_image_alt', + 'host_in_downtime', + 'host_is_flapping', + 'host_is_reachable', + 'host_last_check', + 'host_last_notification', + 'host_last_state_change', + 'host_long_output', + 'host_max_check_attempts', + 'host_name', + 'host_next_check', + 'host_next_update', + 'host_notes', + 'host_notes_url', + 'host_notifications_enabled', + 'host_notifications_enabled_changed', + 'host_obsessing', + 'host_obsessing_changed', + 'host_output', + 'host_passive_checks_enabled', + 'host_passive_checks_enabled_changed', + 'host_percent_state_change', + 'host_perfdata', + 'host_process_perfdata' => 'host_process_performance_data', + 'host_state', + 'host_state_type', + 'instance_name' + ); + return $this->backend->select()->from('hoststatus', $columns) + ->whereEx(new FilterEqual('host_name', '=', $this->host)); + } + + /** + * Fetch the services running on the host + * + * @return $this + */ + public function fetchServices() + { + $services = array(); + foreach ($this->backend->select()->from('servicestatus', array('service_description')) + ->where('host_name', $this->host) + ->applyFilter($this->getFilter()) + ->getQuery() as $service) { + $services[] = new Service($this->backend, $this->host, $service->service_description); + } + $this->services = $services; + return $this; + } + + /** + * Get the optional translated textual representation of a host state + * + * @param int $state + * @param bool $translate + * + * @return string + * @throws InvalidArgumentException If the host state is not valid + */ + public static function getStateText($state, $translate = false) + { + $translate = (bool) $translate; + switch ((int) $state) { + case self::STATE_UP: + $text = $translate ? mt('monitoring', 'UP') : 'up'; + break; + case self::STATE_DOWN: + $text = $translate ? mt('monitoring', 'DOWN') : 'down'; + break; + case self::STATE_UNREACHABLE: + $text = $translate ? mt('monitoring', 'UNREACHABLE') : 'unreachable'; + break; + case self::STATE_PENDING: + $text = $translate ? mt('monitoring', 'PENDING') : 'pending'; + break; + default: + throw new InvalidArgumentException('Invalid host state \'%s\'', $state); + } + return $text; + } + + public function getNotesUrls() + { + return $this->resolveAllStrings( + MonitoredObject::parseAttributeUrls($this->host_notes_url) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Object/HostList.php b/modules/monitoring/library/Monitoring/Object/HostList.php new file mode 100644 index 0000000..8b1947d --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/HostList.php @@ -0,0 +1,133 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\SimpleQuery; +use Icinga\Util\StringHelper; + +/** + * A host list + */ +class HostList extends ObjectList +{ + protected $dataViewName = 'hoststatus'; + + protected $columns = array('host_name'); + + protected function fetchObjects() + { + $hosts = array(); + $query = $this->backend->select()->from($this->dataViewName, $this->columns)->applyFilter($this->filter) + ->getQuery()->getSelectQuery()->query(); + foreach ($query as $row) { + /** @var object $row */ + $host = new Host($this->backend, $row->host_name); + $host->setProperties($row); + $hosts[] = $host; + } + return $hosts; + } + + /** + * Create a state summary of all hosts that can be consumed by hostssummary.phtml + * + * @return SimpleQuery + */ + public function getStateSummary() + { + $hostStates = array_fill_keys(self::getHostStatesSummaryEmpty(), 0); + foreach ($this as $host) { + $unhandled = (bool) $host->problem === true && (bool) $host->handled === false; + + $stateName = 'hosts_' . $host::getStateText($host->state); + ++$hostStates[$stateName]; + ++$hostStates[$stateName. ($unhandled ? '_unhandled' : '_handled')]; + } + + $hostStates['hosts_total'] = count($this); + + $ds = new ArrayDatasource(array((object) $hostStates)); + return $ds->select(); + } + + /** + * Return an empty array with all possible host state names + * + * @return array An array containing all possible host states as keys and 0 as values. + */ + public static function getHostStatesSummaryEmpty() + { + return StringHelper::cartesianProduct( + array( + array('hosts'), + array( + Host::getStateText(Host::STATE_UP), + Host::getStateText(Host::STATE_DOWN), + Host::getStateText(Host::STATE_UNREACHABLE), + Host::getStateText(Host::STATE_PENDING) + ), + array(null, 'handled', 'unhandled') + ), + '_' + ); + } + + /** + * Returns a Filter that matches all hosts in this list + * + * @return Filter + */ + public function objectsFilter($columns = array('host' => 'host')) + { + $filterExpression = array(); + foreach ($this as $host) { + $filterExpression[] = Filter::where($columns['host'], $host->getName()); + } + return FilterOr::matchAny($filterExpression); + } + + /** + * Get the comments + * + * @return \Icinga\Module\Monitoring\DataView\Hostcomment + */ + public function getComments() + { + return $this->backend + ->select() + ->from('hostcomment', array('host_name')) + ->applyFilter(clone $this->filter); + } + + /** + * Get the scheduled downtimes + * + * @return \Icinga\Module\Monitoring\DataView\Hostdowntime + */ + public function getScheduledDowntimes() + { + return $this->backend + ->select() + ->from('hostdowntime', array('host_name')) + ->applyFilter(clone $this->filter); + } + + /** + * @return ObjectList + */ + public function getUnacknowledgedObjects() + { + $unhandledObjects = array(); + foreach ($this as $object) { + if (! in_array((int) $object->state, array(0, 99)) && + (bool) $object->host_acknowledged === false) { + $unhandledObjects[] = $object; + } + } + return $this->newFromArray($unhandledObjects); + } +} diff --git a/modules/monitoring/library/Monitoring/Object/Macro.php b/modules/monitoring/library/Monitoring/Object/Macro.php new file mode 100644 index 0000000..3f67154 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/Macro.php @@ -0,0 +1,82 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use Exception; +use Icinga\Application\Logger; + +/** + * Expand macros in string in the context of MonitoredObjects + */ +class Macro +{ + /** + * Known icinga macros + * + * @var array + */ + private static $icingaMacros = array( + 'HOSTNAME' => 'host_name', + 'HOSTADDRESS' => 'host_address', + 'HOSTADDRESS6' => 'host_address6', + 'SERVICEDESC' => 'service_description', + 'host.name' => 'host_name', + 'host.address' => 'host_address', + 'host.address6' => 'host_address6', + 'service.description' => 'service_description', + 'service.name' => 'service_description' + ); + + /** + * Return the given string with macros being resolved + * + * @param string $input The string in which to look for macros + * @param MonitoredObject|stdClass $object The host or service used to resolve macros + * + * @return string The substituted or unchanged string + */ + public static function resolveMacros($input, $object) + { + $matches = array(); + if (preg_match_all('@\$([^\$\s]+)\$@', $input, $matches)) { + foreach ($matches[1] as $key => $value) { + $newValue = self::resolveMacro($value, $object); + if ($newValue !== $value) { + $input = str_replace($matches[0][$key], $newValue, $input); + } + } + } + + return $input; + } + + /** + * Resolve a macro based on the given object + * + * @param string $macro The macro to resolve + * @param MonitoredObject|stdClass $object The object used to resolve the macro + * + * @return string The new value or the macro if it cannot be resolved + */ + public static function resolveMacro($macro, $object) + { + if (isset(self::$icingaMacros[$macro]) && isset($object->{self::$icingaMacros[$macro]})) { + return $object->{self::$icingaMacros[$macro]}; + } + + try { + $value = $object->$macro; + } catch (Exception $e) { + $objectName = $object->getName(); + if ($object instanceof Service) { + $objectName = $object->getHost()->getName() . '!' . $objectName; + } + + $value = null; + Logger::debug('Unable to resolve macro "%s" on object "%s". An error occured: %s', $macro, $objectName, $e); + } + + return $value !== null ? $value : $macro; + } +} diff --git a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php new file mode 100644 index 0000000..91fd9e7 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php @@ -0,0 +1,930 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use Icinga\Data\Filter\FilterEqual; +use stdClass; +use InvalidArgumentException; +use Icinga\Authentication\Auth; +use Icinga\Application\Config; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use Icinga\Exception\InvalidPropertyException; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Util\GlobFilter; +use Icinga\Web\UrlParams; + +/** + * A monitored Icinga object, i.e. host or service + */ +abstract class MonitoredObject implements Filterable +{ + /** + * Type host + */ + const TYPE_HOST = 'host'; + + /** + * Type service + */ + const TYPE_SERVICE = 'service'; + + /** + * Acknowledgement of the host or service if any + * + * @var object + */ + protected $acknowledgement; + + /** + * Backend to fetch object information from + * + * @var MonitoringBackend + */ + protected $backend; + + /** + * Comments + * + * @var array + */ + protected $comments; + + /** + * This object's obfuscated custom variables + * + * @var array + */ + protected $customvars; + + /** + * This object's obfuscated custom variables, names not lower case + * + * @var array + */ + protected $customvarsWithOriginalNames; + + /** + * The host custom variables + * + * @var array + */ + protected $hostVariables; + + /** + * The service custom variables + * + * @var array + */ + protected $serviceVariables; + + /** + * Contact groups + * + * @var array + */ + protected $contactgroups; + + /** + * Contacts + * + * @var array + */ + protected $contacts; + + /** + * Downtimes + * + * @var array + */ + protected $downtimes; + + /** + * Event history + * + * @var \Icinga\Module\Monitoring\DataView\EventHistory + */ + protected $eventhistory; + + /** + * Filter + * + * @var Filter + */ + protected $filter; + + /** + * Host groups + * + * @var array + */ + protected $hostgroups; + + /** + * Prefix of the Icinga object, i.e. 'host_' or 'service_' + * + * @var string + */ + protected $prefix; + + /** + * Properties + * + * @var object + */ + protected $properties; + + /** + * Service groups + * + * @var array + */ + protected $servicegroups; + + /** + * Type of the Icinga object, i.e. 'host' or 'service' + * + * @var string + */ + protected $type; + + /** + * Stats + * + * @var object + */ + protected $stats; + + /** + * The properties to hide from the user + * + * @var GlobFilter + */ + protected $blacklistedProperties = null; + + /** + * Create a monitored object, i.e. host or service + * + * @param MonitoringBackend $backend Backend to fetch object information from + */ + public function __construct(MonitoringBackend $backend) + { + $this->backend = $backend; + } + + /** + * Get the object's data view + * + * @return \Icinga\Module\Monitoring\DataView\DataView + */ + abstract protected function getDataView(); + + /** + * Get all note urls configured for this monitored object + * + * @return array All note urls as a string + */ + abstract public function getNotesUrls(); + + /** + * {@inheritdoc} + */ + public function addFilter(Filter $filter) + { + // Left out on purpose. Interface is deprecated. + } + + /** + * {@inheritdoc} + */ + public function applyFilter(Filter $filter) + { + $this->getFilter()->addFilter($filter); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFilter() + { + if ($this->filter === null) { + $this->filter = Filter::matchAll(); + } + + return $this->filter; + } + + /** + * {@inheritdoc} + */ + public function setFilter(Filter $filter) + { + // Left out on purpose. Interface is deprecated. + } + + /** + * {@inheritdoc} + */ + public function where($condition, $value = null) + { + // Left out on purpose. Interface is deprecated. + } + + /** + * Require the object's type to be one of the given types + * + * @param array $oneOf + * + * @return bool + * @throws InvalidArgumentException If the object's type is not one of the given types. + */ + public function assertOneOf(array $oneOf) + { + if (! in_array($this->type, $oneOf)) { + throw new InvalidArgumentException; + } + return true; + } + + /** + * Fetch the object's properties + * + * @return bool + */ + public function fetch() + { + $properties = $this->getDataView()->applyFilter($this->getFilter())->getQuery()->fetchRow(); + + if ($properties === false) { + return false; + } + + if (isset($properties->host_contacts)) { + $this->contacts = array(); + foreach (preg_split('~,~', $properties->host_contacts) as $contact) { + $this->contacts[] = (object) array( + 'contact_name' => $contact, + 'contact_alias' => $contact, + 'contact_email' => null, + 'contact_pager' => null, + ); + } + } + + $this->properties = $properties; + + return true; + } + + /** + * Fetch the object's acknowledgement + */ + public function fetchAcknowledgement() + { + if ($this->comments === null) { + $this->fetchComments(); + } + + return $this; + } + + /** + * Fetch the object's comments + * + * @return $this + */ + public function fetchComments() + { + $commentsView = $this->backend->select()->from('comment', array( + 'author' => 'comment_author_name', + 'comment' => 'comment_data', + 'expiration' => 'comment_expiration', + 'id' => 'comment_internal_id', + 'name' => 'comment_name', + 'persistent' => 'comment_is_persistent', + 'timestamp' => 'comment_timestamp', + 'type' => 'comment_type' + )); + if ($this->type === self::TYPE_SERVICE) { + $commentsView + ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name)) + ->whereEx(new FilterEqual('service_description', '=', $this->service_description)); + } else { + $commentsView->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + } + $commentsView + ->whereEx(new FilterEqual('comment_type', '=', ['ack', 'comment'])) + ->whereEx(new FilterEqual('object_type', '=', $this->type)); + + $comments = $commentsView->fetchAll(); + + if ((bool) $this->properties->{$this->prefix . 'acknowledged'}) { + $ackCommentIdx = null; + + foreach ($comments as $i => $comment) { + if ($comment->type === 'ack') { + $this->acknowledgement = new Acknowledgement(array( + 'author' => $comment->author, + 'comment' => $comment->comment, + 'entry_time' => $comment->timestamp, + 'expiration_time' => $comment->expiration, + 'sticky' => (int) $this->properties->{$this->prefix . 'acknowledgement_type'} === 2 + )); + $ackCommentIdx = $i; + break; + } + } + + if ($ackCommentIdx !== null) { + unset($comments[$ackCommentIdx]); + } + } + + $this->comments = $comments; + + return $this; + } + + /** + * Fetch the object's contact groups + * + * @return $this + */ + public function fetchContactgroups() + { + $contactsGroups = $this->backend->select()->from('contactgroup', array( + 'contactgroup_name', + 'contactgroup_alias' + )); + if ($this->type === self::TYPE_SERVICE) { + $contactsGroups + ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name)) + ->whereEx(new FilterEqual('service_description', '=', $this->service_description)); + } else { + $contactsGroups->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + } + $this->contactgroups = $contactsGroups; + return $this; + } + + /** + * Fetch the object's contacts + * + * @return $this + */ + public function fetchContacts() + { + $contacts = $this->backend->select()->from("{$this->type}contact", array( + 'contact_name', + 'contact_alias', + 'contact_email', + 'contact_pager', + )); + if ($this->type === self::TYPE_SERVICE) { + $contacts + ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name)) + ->whereEx(new FilterEqual('service_description', '=', $this->service_description)); + } else { + $contacts->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + } + $this->contacts = $contacts; + return $this; + } + + /** + * Fetch this object's obfuscated custom variables + * + * @return $this + */ + public function fetchCustomvars() + { + + if ($this->type === self::TYPE_SERVICE) { + $this->fetchServiceVariables(); + $customvars = $this->serviceVariables; + } else { + $this->fetchHostVariables(); + $customvars = $this->hostVariables; + } + + $this->customvars = $customvars; + $this->hideBlacklistedProperties(); + $this->customvars = $this->obfuscateCustomVars($this->customvars, null); + $this->customvarsWithOriginalNames = $this->obfuscateCustomVars($this->customvarsWithOriginalNames, null); + + return $this; + } + + /** + * Obfuscate custom variables recursively + * + * @param stdClass|array $customvars The custom variables to obfuscate + * + * @return stdClass|array The obfuscated custom variables + */ + protected function obfuscateCustomVars($customvars, $_) + { + return self::protectCustomVars($customvars); + } + + public static function protectCustomVars($customvars) + { + $blacklist = []; + $blacklistPattern = ''; + + if (($blacklistConfig = Config::module('monitoring')->get('security', 'protected_customvars', '')) !== '') { + foreach (explode(',', $blacklistConfig) as $customvar) { + $nonWildcards = array(); + foreach (explode('*', $customvar) as $nonWildcard) { + $nonWildcards[] = preg_quote($nonWildcard, '/'); + } + $blacklist[] = implode('.*', $nonWildcards); + } + $blacklistPattern = '/^(' . implode('|', $blacklist) . ')$/i'; + } + + if (! $blacklistPattern) { + return $customvars; + } + + $obfuscator = function ($vars) use ($blacklistPattern, &$obfuscator) { + $result = []; + foreach ($vars as $name => $value) { + if ($blacklistPattern && preg_match($blacklistPattern, $name)) { + $result[$name] = '***'; + } elseif ($value instanceof stdClass || is_array($value)) { + $obfuscated = $obfuscator($value); + $result[$name] = $value instanceof stdClass ? (object) $obfuscated : $obfuscated; + } else { + $result[$name] = $value; + } + } + + return $result; + }; + $obfuscatedCustomVars = $obfuscator($customvars); + + return $customvars instanceof stdClass ? (object) $obfuscatedCustomVars : $obfuscatedCustomVars; + } + + /** + * Hide all blacklisted properties from the user as restricted by monitoring/blacklist/properties + * + * Currently this only affects the custom variables + */ + protected function hideBlacklistedProperties() + { + if ($this->blacklistedProperties === null) { + $this->blacklistedProperties = new GlobFilter( + Auth::getInstance()->getRestrictions('monitoring/blacklist/properties') + ); + } + + $allProperties = $this->blacklistedProperties->removeMatching( + [$this->type => ['vars' => $this->customvars]] + ); + $this->customvars = isset($allProperties[$this->type]['vars']) + ? $allProperties[$this->type]['vars'] + : []; + + $allProperties = $this->blacklistedProperties->removeMatching( + [$this->type => ['vars' => $this->customvarsWithOriginalNames]] + ); + $this->customvarsWithOriginalNames = isset($allProperties[$this->type]['vars']) + ? $allProperties[$this->type]['vars'] + : []; + } + + /** + * Fetch the host custom variables related to this object + * + * @return $this + */ + public function fetchHostVariables() + { + $query = $this->backend->select()->from('customvar', array( + 'varname', + 'varvalue', + 'is_json' + )) + ->whereEx(new FilterEqual('object_type', '=', static::TYPE_HOST)) + ->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + + $this->hostVariables = []; + + if ($this->type === static::TYPE_HOST) { + $this->customvarsWithOriginalNames = []; + } + + foreach ($query as $row) { + if ($row->is_json) { + $this->hostVariables[strtolower($row->varname)] = json_decode($row->varvalue); + } else { + $this->hostVariables[strtolower($row->varname)] = $row->varvalue; + } + + if ($this->type === static::TYPE_HOST) { + $this->customvarsWithOriginalNames[$row->varname] = $this->hostVariables[strtolower($row->varname)]; + } + } + + return $this; + } + + /** + * Fetch the service custom variables related to this object + * + * @return $this + * + * @throws ProgrammingError In case this object is not a service + */ + public function fetchServiceVariables() + { + if ($this->type !== static::TYPE_SERVICE) { + throw new ProgrammingError('Cannot fetch service custom variables for non-service objects'); + } + + $query = $this->backend->select()->from('customvar', array( + 'varname', + 'varvalue', + 'is_json' + )) + ->whereEx(new FilterEqual('object_type', '=', static::TYPE_SERVICE)) + ->whereEx(new FilterEqual('host_name', '=', $this->host_name)) + ->whereEx(new FilterEqual('service_description', '=', $this->service_description)); + + $this->serviceVariables = []; + $this->customvarsWithOriginalNames = []; + foreach ($query as $row) { + if ($row->is_json) { + $this->customvarsWithOriginalNames[$row->varname] = json_decode($row->varvalue); + $this->serviceVariables[strtolower($row->varname)] = $this->customvarsWithOriginalNames[$row->varname]; + } else { + $this->serviceVariables[strtolower($row->varname)] = $row->varvalue; + $this->customvarsWithOriginalNames[$row->varname] = $row->varvalue; + } + } + + return $this; + } + + /** + * Fetch the object's downtimes + * + * @return $this + */ + public function fetchDowntimes() + { + $downtimes = $this->backend->select()->from('downtime', array( + 'author_name' => 'downtime_author_name', + 'comment' => 'downtime_comment', + 'duration' => 'downtime_duration', + 'end' => 'downtime_end', + 'entry_time' => 'downtime_entry_time', + 'id' => 'downtime_internal_id', + 'is_fixed' => 'downtime_is_fixed', + 'is_flexible' => 'downtime_is_flexible', + 'is_in_effect' => 'downtime_is_in_effect', + 'name' => 'downtime_name', + 'objecttype' => 'object_type', + 'scheduled_end' => 'downtime_scheduled_end', + 'scheduled_start' => 'downtime_scheduled_start', + 'start' => 'downtime_start' + )) + ->whereEx(new FilterEqual('object_type', '=', $this->type)) + ->order('downtime_is_in_effect', 'DESC') + ->order('downtime_scheduled_start', 'ASC'); + if ($this->type === self::TYPE_SERVICE) { + $downtimes + ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name)) + ->whereEx(new FilterEqual('service_description', '=', $this->service_description)); + } else { + $downtimes + ->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + } + $this->downtimes = $downtimes->getQuery()->fetchAll(); + return $this; + } + + /** + * Fetch the object's event history + * + * @return $this + */ + public function fetchEventhistory() + { + $eventHistory = $this->backend + ->select() + ->from( + 'eventhistory', + array( + 'id', + 'object_type', + 'host_name', + 'host_display_name', + 'service_description', + 'service_display_name', + 'timestamp', + 'state', + 'output', + 'type' + ) + ) + ->whereEx(new FilterEqual('object_type', '=', $this->type)) + ->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + + if ($this->type === self::TYPE_SERVICE) { + $eventHistory->whereEx( + new FilterEqual('service_description', '=', $this->service_description) + ); + } + + $this->eventhistory = $eventHistory; + return $this; + } + + /** + * Fetch the object's host groups + * + * @return $this + */ + public function fetchHostgroups() + { + $this->hostgroups = $this->backend->select() + ->from('hostgroup', array('hostgroup_name', 'hostgroup_alias')) + ->whereEx(new FilterEqual('host_name', '=', $this->host_name)) + ->applyFilter($this->getFilter()) + ->fetchPairs(); + return $this; + } + + /** + * Fetch the object's service groups + * + * @return $this + */ + public function fetchServicegroups() + { + $query = $this->backend->select() + ->from('servicegroup', array('servicegroup_name', 'servicegroup_alias')) + ->whereEx(new FilterEqual('host_name', '=', $this->host_name)); + + if ($this->type === self::TYPE_SERVICE) { + $query->whereEx( + new FilterEqual('service_description', '=', $this->service_description) + ); + } + + $this->servicegroups = $query->applyFilter($this->getFilter())->fetchPairs(); + return $this; + } + + /** + * Fetch stats + * + * @return $this + */ + public function fetchStats() + { + $this->stats = $this->backend->select()->from('servicestatussummary', array( + 'services_total', + 'services_ok', + 'services_critical', + 'services_critical_unhandled', + 'services_critical_handled', + 'services_warning', + 'services_warning_unhandled', + 'services_warning_handled', + 'services_unknown', + 'services_unknown_unhandled', + 'services_unknown_handled', + 'services_pending', + )) + ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name)) + ->applyFilter($this->getFilter()) + ->fetchRow(); + return $this; + } + + /** + * Get all action urls configured for this monitored object + * + * @return array All note urls as a string + */ + public function getActionUrls() + { + return $this->resolveAllStrings( + MonitoredObject::parseAttributeUrls($this->action_url) + ); + } + + /** + * Get the type of the object + * + * @param bool $translate + * + * @return string + */ + public function getType($translate = false) + { + if ($translate !== false) { + switch ($this->type) { + case self::TYPE_HOST: + $type = mt('montiroing', 'host'); + break; + case self::TYPE_SERVICE: + $type = mt('monitoring', 'service'); + break; + default: + throw new InvalidArgumentException('Invalid type ' . $this->type); + } + } else { + $type = $this->type; + } + return $type; + } + + /** + * Parse the content of the action_url or notes_url attributes + * + * Find all occurences of http links, separated by whitespaces and quoted + * by single or double-ticks. + * + * @link http://docs.icinga.com/latest/de/objectdefinitions.html + * + * @param string $urlString A string containing one or more urls + * @return array Array of urls as strings + */ + public static function parseAttributeUrls($urlString) + { + if (empty($urlString)) { + return array(); + } + $links = array(); + if (strpos($urlString, "' ") === false) { + $links[] = $urlString; + } else { + // parse notes-url format + foreach (explode("' ", $urlString) as $url) { + $url = strpos($url, "'") === 0 ? substr($url, 1) : $url; + $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url; + $links[] = $url; + } + } + return $links; + } + + /** + * Fetch all available data of the object + * + * @return $this + */ + public function populate() + { + $this + ->fetchComments() + ->fetchContactgroups() + ->fetchContacts() + ->fetchCustomvars() + ->fetchDowntimes(); + + // Call fetchHostgroups or fetchServicegroups depending on the object's type + $fetchGroups = 'fetch' . ucfirst($this->type) . 'groups'; + $this->$fetchGroups(); + + return $this; + } + + /** + * Resolve macros in all given strings in the current object context + * + * @param array $strs An array of urls as string + * + * @return array + */ + protected function resolveAllStrings(array $strs) + { + foreach ($strs as $i => $str) { + $strs[$i] = Macro::resolveMacros($str, $this); + } + return $strs; + } + + /** + * Set the object's properties + * + * @param object $properties + * + * @return $this + */ + public function setProperties($properties) + { + $this->properties = (object) $properties; + return $this; + } + + public function __isset($name) + { + if (property_exists($this->properties, $name)) { + return isset($this->properties->$name); + } elseif (property_exists($this, $name)) { + return isset($this->$name); + } + return false; + } + + public function __get($name) + { + if (property_exists($this->properties, $name)) { + return $this->properties->$name; + } elseif (property_exists($this, $name)) { + if ($this->$name === null) { + $fetchMethod = 'fetch' . ucfirst($name); + $this->$fetchMethod(); + } + + return $this->$name; + } elseif (preg_match('/^_(host|service)_(.+)/i', $name, $matches)) { + if (strtolower($matches[1]) === static::TYPE_HOST) { + if ($this->hostVariables === null) { + $this->fetchHostVariables(); + } + + $customvars = $this->hostVariables; + } else { + if ($this->serviceVariables === null) { + $this->fetchServiceVariables(); + } + + $customvars = $this->serviceVariables; + } + + $variableName = strtolower($matches[2]); + if (isset($customvars[$variableName])) { + return $customvars[$variableName]; + } + + return null; // Unknown custom variables MUST NOT throw an error + } elseif (in_array($name, array('contact_name', 'contactgroup_name', 'hostgroup_name', 'servicegroup_name'))) { + if ($name === 'contact_name') { + if ($this->contacts === null) { + $this->fetchContacts(); + } + + return array_map(function ($el) { + return $el->contact_name; + }, $this->contacts); + } elseif ($name === 'contactgroup_name') { + if ($this->contactgroups === null) { + $this->fetchContactgroups(); + } + + return array_map(function ($el) { + return $el->contactgroup_name; + }, $this->contactgroups); + } elseif ($name === 'hostgroup_name') { + if ($this->hostgroups === null) { + $this->fetchHostgroups(); + } + + return array_keys($this->hostgroups); + } else { // $name === 'servicegroup_name' + if ($this->servicegroups === null) { + $this->fetchServicegroups(); + } + + return array_keys($this->servicegroups); + } + } elseif (strpos($name, $this->prefix) !== 0) { + $propertyName = strtolower($name); + $prefixedName = $this->prefix . $propertyName; + if (property_exists($this->properties, $prefixedName)) { + return $this->properties->$prefixedName; + } + + if ($this->type === static::TYPE_HOST) { + if ($this->hostVariables === null) { + $this->fetchHostVariables(); + } + + $customvars = $this->hostVariables; + } else { // $this->type === static::TYPE_SERVICE + if ($this->serviceVariables === null) { + $this->fetchServiceVariables(); + } + + $customvars = $this->serviceVariables; + } + + if (isset($customvars[$propertyName])) { + return $customvars[$propertyName]; + } + } + + throw new InvalidPropertyException('Can\'t access property \'%s\'. Property does not exist.', $name); + } +} diff --git a/modules/monitoring/library/Monitoring/Object/ObjectList.php b/modules/monitoring/library/Monitoring/Object/ObjectList.php new file mode 100644 index 0000000..36b922a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/ObjectList.php @@ -0,0 +1,293 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use ArrayIterator; +use Countable; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use IteratorAggregate; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Traversable; + +abstract class ObjectList implements Countable, IteratorAggregate, Filterable +{ + /** + * @var string + */ + protected $dataViewName; + + /** + * @var MonitoringBackend + */ + protected $backend; + + /** + * @var array + */ + protected $columns; + + /** + * @var Filter + */ + protected $filter; + + /** + * @var array + */ + protected $objects; + + /** + * @var int + */ + protected $count; + + public function __construct(MonitoringBackend $backend) + { + $this->backend = $backend; + } + + /** + * @param array $columns + * + * @return $this + */ + public function setColumns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * @param Filter $filter + * + * @return $this + */ + public function setFilter(Filter $filter) + { + $this->filter = $filter; + return $this; + } + + /** + * @return Filter + */ + public function getFilter() + { + if ($this->filter === null) { + $this->filter = Filter::matchAll(); + } + + return $this->filter; + } + + public function applyFilter(Filter $filter) + { + $this->getFilter()->addFilter($filter); + return $this; + } + + public function addFilter(Filter $filter) + { + $this->getFilter()->addFilter($filter); + } + + public function where($condition, $value = null) + { + $this->getFilter()->addFilter(Filter::where($condition, $value)); + } + + abstract protected function fetchObjects(); + + /** + * @return array + */ + public function fetch() + { + if ($this->objects === null) { + $this->objects = $this->fetchObjects(); + } + return $this->objects; + } + + public function count(): int + { + if ($this->count === null) { + $this->count = (int) $this->backend + ->select() + ->from($this->dataViewName, $this->columns) + ->applyFilter($this->filter) + ->getQuery() + ->count(); + } + + return $this->count; + } + + public function getIterator(): Traversable + { + if ($this->objects === null) { + $this->fetch(); + } + return new ArrayIterator($this->objects); + } + + /** + * Get the comments + * + * @return \Icinga\Module\Monitoring\DataView\Comment + */ + public function getComments() + { + return $this->backend->select()->from('comment')->applyFilter($this->filter); + } + + /** + * Get the scheduled downtimes + * + * @return type + */ + public function getScheduledDowntimes() + { + return $this->backend->select()->from('downtime')->applyFilter($this->filter); + } + + /** + * @return ObjectList + */ + public function getAcknowledgedObjects() + { + $acknowledgedObjects = array(); + foreach ($this as $object) { + if ((bool) $object->acknowledged === true) { + $acknowledgedObjects[] = $object; + } + } + return $this->newFromArray($acknowledgedObjects); + } + + /** + * @return ObjectList + */ + public function getObjectsInDowntime() + { + $objectsInDowntime = array(); + foreach ($this as $object) { + if ((bool) $object->in_downtime === true) { + $objectsInDowntime[] = $object; + } + } + return $this->newFromArray($objectsInDowntime); + } + + /** + * @return ObjectList + */ + public function getUnhandledObjects() + { + $unhandledObjects = array(); + foreach ($this as $object) { + if ((bool) $object->problem === true && (bool) $object->handled === false) { + $unhandledObjects[] = $object; + } + } + return $this->newFromArray($unhandledObjects); + } + + /** + * @return ObjectList + */ + public function getProblemObjects() + { + $handledObjects = array(); + foreach ($this as $object) { + if ((bool) $object->problem === true) { + $handledObjects[] = $object; + } + } + return $this->newFromArray($handledObjects); + } + + /** + * @return ObjectList + */ + abstract public function getUnacknowledgedObjects(); + + /** + * Create a ObjectList from an array of hosts without querying a backend + * + * @return ObjectList + */ + protected function newFromArray(array $objects) + { + $class = get_called_class(); + $list = new $class($this->backend); + $list->objects = $objects; + $list->count = count($objects); + $list->filter = $list->objectsFilter(); + return $list; + } + + /** + * Create a filter that matches exactly the elements of this object list + * + * @param array $columns Override default column names. + * + * @return Filter + */ + abstract public function objectsFilter($columns = array()); + + /** + * Get the feature status + * + * @return array + */ + public function getFeatureStatus() + { + // null - init + // 0 - disabled + // 1 - enabled + // 2 - enabled & disabled + $featureStatus = array( + 'active_checks_enabled' => null, + 'passive_checks_enabled' => null, + 'obsessing' => null, + 'notifications_enabled' => null, + 'event_handler_enabled' => null, + 'flap_detection_enabled' => null + ); + + $features = array(); + + foreach ($featureStatus as $feature => &$status) { + $features[$feature] = &$status; + } + + foreach ($this as $object) { + foreach ($features as $feature => &$status) { + $enabled = (int) $object->{$feature}; + if (! isset($status)) { + $status = $enabled; + } elseif ($status !== $enabled) { + $status = 2; + unset($features[$status]); + if (empty($features)) { + break 2; + } + break; + } + } + } + + return $featureStatus; + } +} diff --git a/modules/monitoring/library/Monitoring/Object/Service.php b/modules/monitoring/library/Monitoring/Object/Service.php new file mode 100644 index 0000000..95c00fc --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/Service.php @@ -0,0 +1,219 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use Icinga\Data\Filter\FilterEqual; +use InvalidArgumentException; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; + +/** + * An Icinga service + */ +class Service extends MonitoredObject +{ + /** + * Service state 'OK' + */ + const STATE_OK = 0; + + /** + * Service state 'WARNING' + */ + const STATE_WARNING = 1; + + /** + * Service state 'CRITICAL' + */ + const STATE_CRITICAL = 2; + + /** + * Service state 'UNKNOWN' + */ + const STATE_UNKNOWN = 3; + + /** + * Service state 'PENDING' + */ + const STATE_PENDING = 99; + + /** + * Type of the Icinga service + * + * @var string + */ + public $type = self::TYPE_SERVICE; + + /** + * Prefix of the Icinga service + * + * @var string + */ + public $prefix = 'service_'; + + /** + * Host the service is running on + * + * @var Host + */ + protected $host; + + /** + * Service name + * + * @var string + */ + protected $service; + + /** + * Create a new service + * + * @param MonitoringBackend $backend Backend to fetch service information from + * @param string $host Hostname the service is running on + * @param string $service Service name + */ + public function __construct(MonitoringBackend $backend, $host, $service) + { + parent::__construct($backend); + $this->host = new Host($backend, $host); + $this->service = $service; + } + + /** + * Get the host the service is running on + * + * @return Host + */ + public function getHost() + { + return $this->host; + } + + /** + * Get the service name + * + * @return string + */ + public function getName() + { + return $this->service; + } + + /** + * Get the data view + * + * @return \Icinga\Module\Monitoring\DataView\ServiceStatus + */ + protected function getDataView() + { + return $this->backend->select()->from('servicestatus', array( + 'instance_name', + 'host_attempt', + 'host_icon_image', + 'host_icon_image_alt', + 'host_acknowledged', + 'host_active_checks_enabled', + 'host_address', + 'host_address6', + 'host_alias', + 'host_display_name', + 'host_handled', + 'host_in_downtime', + 'host_is_flapping', + 'host_last_state_change', + 'host_name', + 'host_notifications_enabled', + 'host_passive_checks_enabled', + 'host_state', + 'host_state_type', + 'service_icon_image', + 'service_icon_image_alt', + 'service_acknowledged', + 'service_acknowledgement_type', + 'service_action_url', + 'service_active_checks_enabled', + 'service_active_checks_enabled_changed', + 'service_attempt', + 'service_check_command', + 'service_check_execution_time', + 'service_check_interval', + 'service_check_latency', + 'service_check_source', + 'service_check_timeperiod', + 'service_current_notification_number', + 'service_description', + 'service_display_name', + 'service_event_handler_enabled', + 'service_event_handler_enabled_changed', + 'service_flap_detection_enabled', + 'service_flap_detection_enabled_changed', + 'service_handled', + 'service_in_downtime', + 'service_is_flapping', + 'service_is_reachable', + 'service_last_check', + 'service_last_notification', + 'service_last_state_change', + 'service_long_output', + 'service_next_check', + 'service_next_update', + 'service_notes', + 'service_notes_url', + 'service_notifications_enabled', + 'service_notifications_enabled_changed', + 'service_obsessing', + 'service_obsessing_changed', + 'service_output', + 'service_passive_checks_enabled', + 'service_passive_checks_enabled_changed', + 'service_percent_state_change', + 'service_perfdata', + 'service_process_perfdata' => 'service_process_performance_data', + 'service_state', + 'service_state_type' + )) + ->whereEx(new FilterEqual('host_name', '=', $this->host->getName())) + ->whereEx(new FilterEqual('service_description', '=', $this->service)); + } + + /** + * Get the optional translated textual representation of a service state + * + * @param int $state + * @param bool $translate + * + * @return string + * @throws InvalidArgumentException If the service state is not valid + */ + public static function getStateText($state, $translate = false) + { + $translate = (bool) $translate; + switch ((int) $state) { + case self::STATE_OK: + $text = $translate ? mt('monitoring', 'OK') : 'ok'; + break; + case self::STATE_WARNING: + $text = $translate ? mt('monitoring', 'WARNING') : 'warning'; + break; + case self::STATE_CRITICAL: + $text = $translate ? mt('monitoring', 'CRITICAL') : 'critical'; + break; + case self::STATE_UNKNOWN: + $text = $translate ? mt('monitoring', 'UNKNOWN') : 'unknown'; + break; + case self::STATE_PENDING: + $text = $translate ? mt('monitoring', 'PENDING') : 'pending'; + break; + default: + throw new InvalidArgumentException('Invalid service state \'%s\'', $state); + } + return $text; + } + + public function getNotesUrls() + { + return $this->resolveAllStrings( + MonitoredObject::parseAttributeUrls($this->service_notes_url) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Object/ServiceList.php b/modules/monitoring/library/Monitoring/Object/ServiceList.php new file mode 100644 index 0000000..5bc0bdb --- /dev/null +++ b/modules/monitoring/library/Monitoring/Object/ServiceList.php @@ -0,0 +1,184 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Object; + +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterOr; +use Icinga\Data\SimpleQuery; +use Icinga\Util\StringHelper; + +/** + * A service list + */ +class ServiceList extends ObjectList +{ + protected $hostStateSummary; + + protected $serviceStateSummary; + + protected $dataViewName = 'servicestatus'; + + protected $columns = array('host_name', 'service_description'); + + protected function fetchObjects() + { + $services = array(); + $query = $this->backend->select()->from($this->dataViewName, $this->columns)->applyFilter($this->filter) + ->getQuery()->getSelectQuery()->query(); + foreach ($query as $row) { + /** @var object $row */ + $service = new Service($this->backend, $row->host_name, $row->service_description); + $service->setProperties($row); + $services[] = $service; + } + return $services; + } + + /** + * Create a state summary of all services that can be consumed by servicesummary.phtml + * + * @return SimpleQuery + */ + public function getServiceStateSummary() + { + if (! $this->serviceStateSummary) { + $this->initStateSummaries(); + } + + $ds = new ArrayDatasource(array((object) $this->serviceStateSummary)); + return $ds->select(); + } + + /** + * Create a state summary of all hosts that can be consumed by hostsummary.phtml + * + * @return SimpleQuery + */ + public function getHostStateSummary() + { + if (! $this->hostStateSummary) { + $this->initStateSummaries(); + } + + $ds = new ArrayDatasource(array((object) $this->hostStateSummary)); + return $ds->select(); + } + + /** + * Calculate the current state summary and populate hostStateSummary and serviceStateSummary + * properties + */ + protected function initStateSummaries() + { + $serviceStates = array_fill_keys(self::getServiceStatesSummaryEmpty(), 0); + $hostStates = array_fill_keys(HostList::getHostStatesSummaryEmpty(), 0); + + foreach ($this as $service) { + $unhandled = false; + if ((bool) $service->problem === true && (bool) $service->handled === false) { + $unhandled = true; + } + + $stateName = 'services_' . $service::getStateText($service->state); + ++$serviceStates[$stateName]; + ++$serviceStates[$stateName . ($unhandled ? '_unhandled' : '_handled')]; + + if (! isset($knownHostStates[$service->getHost()->getName()])) { + $unhandledHost = (bool) $service->host_problem === true && (bool) $service->host_handled === false; + ++$hostStates['hosts_' . $service->getHost()->getStateText($service->host_state)]; + ++$hostStates['hosts_' . $service->getHost()->getStateText($service->host_state) + . ($unhandledHost ? '_unhandled' : '_handled')]; + $knownHostStates[$service->getHost()->getName()] = true; + } + } + + $serviceStates['services_total'] = count($this); + $this->hostStateSummary = $hostStates; + $this->serviceStateSummary = $serviceStates; + } + + /** + * Return an empty array with all possible host state names + * + * @return array An array containing all possible host states as keys and 0 as values. + */ + public static function getServiceStatesSummaryEmpty() + { + return StringHelper::cartesianProduct( + array( + array('services'), + array( + Service::getStateText(Service::STATE_OK), + Service::getStateText(Service::STATE_WARNING), + Service::getStateText(Service::STATE_CRITICAL), + Service::getStateText(Service::STATE_UNKNOWN), + Service::getStateText(Service::STATE_PENDING) + ), + array(null, 'handled', 'unhandled') + ), + '_' + ); + } + + /** + * Returns a Filter that matches all hosts in this HostList + * + * @param array $columns Override filter column names + * + * @return Filter + */ + public function objectsFilter($columns = array('host' => 'host', 'service' => 'service')) + { + $filterExpression = array(); + foreach ($this as $service) { + $filterExpression[] = Filter::matchAll( + Filter::where($columns['host'], $service->getHost()->getName()), + Filter::where($columns['service'], $service->getName()) + ); + } + return FilterOr::matchAny($filterExpression); + } + + /** + * Get the comments + * + * @return \Icinga\Module\Monitoring\DataView\Hostcomment + */ + public function getComments() + { + return $this->backend + ->select() + ->from('servicecomment', array('host_name', 'service_description')) + ->applyFilter(clone $this->filter); + } + + /** + * Get the scheduled downtimes + * + * @return \Icinga\Module\Monitoring\DataView\Servicedowntime + */ + public function getScheduledDowntimes() + { + return $this->backend + ->select() + ->from('servicedowntime', array('host_name', 'service_description')) + ->applyFilter(clone $this->filter); + } + + /** + * @return ObjectList + */ + public function getUnacknowledgedObjects() + { + $unhandledObjects = array(); + foreach ($this as $object) { + if (! in_array((int) $object->state, array(0, 99)) && + (bool) $object->service_acknowledged === false) { + $unhandledObjects[] = $object; + } + } + return $this->newFromArray($unhandledObjects); + } +} diff --git a/modules/monitoring/library/Monitoring/Plugin.php b/modules/monitoring/library/Monitoring/Plugin.php new file mode 100644 index 0000000..e8e1f5d --- /dev/null +++ b/modules/monitoring/library/Monitoring/Plugin.php @@ -0,0 +1,12 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring; + +use Icinga\Application\Cli; + +require_once ICINGA_LIBDIR . '/Icinga/Application/Cli.php'; + +class Plugin extends Cli +{ +} diff --git a/modules/monitoring/library/Monitoring/Plugin/Perfdata.php b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php new file mode 100644 index 0000000..476354a --- /dev/null +++ b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php @@ -0,0 +1,550 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Plugin; + +use Icinga\Util\Format; +use InvalidArgumentException; +use Icinga\Exception\ProgrammingError; +use Icinga\Web\Widget\Chart\InlinePie; +use Icinga\Module\Monitoring\Object\Service; +use Zend_Controller_Front; + +class Perfdata +{ + const PERFDATA_OK = 'ok'; + const PERFDATA_WARNING = 'warning'; + const PERFDATA_CRITICAL = 'critical'; + + /** + * The performance data value being parsed + * + * @var string + */ + protected $perfdataValue; + + /** + * Unit of measurement (UOM) + * + * @var string + */ + protected $unit; + + /** + * The label + * + * @var string + */ + protected $label; + + /** + * The value + * + * @var float + */ + protected $value; + + /** + * The minimum value + * + * @var float + */ + protected $minValue; + + /** + * The maximum value + * + * @var float + */ + protected $maxValue; + + /** + * The WARNING threshold + * + * @var ThresholdRange + */ + protected $warningThreshold; + + /** + * The CRITICAL threshold + * + * @var ThresholdRange + */ + protected $criticalThreshold; + + /** + * Create a new Perfdata object based on the given performance data label and value + * + * @param string $label The perfdata label + * @param string $value The perfdata value + */ + public function __construct($label, $value) + { + $this->perfdataValue = $value; + $this->label = $label; + $this->parse(); + + if ($this->unit === '%') { + if ($this->minValue === null) { + $this->minValue = 0.0; + } + if ($this->maxValue === null) { + $this->maxValue = 100.0; + } + } + + $warn = $this->warningThreshold->getMax(); + if ($warn !== null) { + $crit = $this->criticalThreshold->getMax(); + if ($crit !== null && $warn > $crit) { + $this->warningThreshold->setInverted(); + $this->criticalThreshold->setInverted(); + } + } + } + + /** + * Return a new Perfdata object based on the given performance data key=value pair + * + * @param string $perfdata The key=value pair to parse + * + * @return Perfdata + * + * @throws InvalidArgumentException In case the given performance data has no content or a invalid format + */ + public static function fromString($perfdata) + { + if (empty($perfdata)) { + throw new InvalidArgumentException('Perfdata::fromString expects a string with content'); + } elseif (strpos($perfdata, '=') === false) { + throw new InvalidArgumentException( + 'Perfdata::fromString expects a key=value formatted string. Got "' . $perfdata . '" instead' + ); + } + + list($label, $value) = explode('=', $perfdata, 2); + return new static(trim($label), trim($value)); + } + + /** + * Return whether this performance data's value is a number + * + * @return bool True in case it's a number, otherwise False + */ + public function isNumber() + { + return $this->unit === null; + } + + /** + * Return whether this performance data's value are seconds + * + * @return bool True in case it's seconds, otherwise False + */ + public function isSeconds() + { + return in_array($this->unit, array('s', 'ms', 'us')); + } + + /** + * Return whether this performance data's value is a temperature + * + * @return bool True in case it's temperature, otherwise False + */ + public function isTemperature() + { + return in_array($this->unit, array('°c', '°f')); + } + + /** + * Return whether this performance data's value is in percentage + * + * @return bool True in case it's in percentage, otherwise False + */ + public function isPercentage() + { + return $this->unit === '%'; + } + + /** + * Return whether this performance data's value is in bytes + * + * @return bool True in case it's in bytes, otherwise False + */ + public function isBytes() + { + return in_array($this->unit, array('b', 'kb', 'mb', 'gb', 'tb')); + } + + /** + * Return whether this performance data's value is a counter + * + * @return bool True in case it's a counter, otherwise False + */ + public function isCounter() + { + return $this->unit === 'c'; + } + + /** + * Returns whether it is possible to display a visual representation + * + * @return bool True when the perfdata is visualizable + */ + public function isVisualizable() + { + return isset($this->minValue) && isset($this->maxValue) && isset($this->value); + } + + /** + * Return this perfomance data's label + */ + public function getLabel() + { + return $this->label; + } + + /** + * Return the value or null if it is unknown (U) + * + * @return null|float + */ + public function getValue() + { + return $this->value; + } + + /** + * Return the unit as a string + * + * @return string + */ + public function getUnit() + { + return $this->unit; + } + + /** + * Return the value as percentage (0-100) + * + * @return null|float + */ + public function getPercentage() + { + if ($this->isPercentage()) { + return $this->value; + } + + if ($this->maxValue !== null) { + $minValue = $this->minValue !== null ? $this->minValue : 0.0; + if ($this->maxValue == $minValue) { + return null; + } + + if ($this->value > $minValue) { + return (($this->value - $minValue) / ($this->maxValue - $minValue)) * 100; + } + } + } + + /** + * Return this performance data's warning treshold + * + * @return ThresholdRange + */ + public function getWarningThreshold() + { + return $this->warningThreshold; + } + + /** + * Return this performance data's critical treshold + * + * @return ThresholdRange + */ + public function getCriticalThreshold() + { + return $this->criticalThreshold; + } + + /** + * Return the minimum value or null if it is not available + * + * @return null|string + */ + public function getMinimumValue() + { + return $this->minValue; + } + + /** + * Return the maximum value or null if it is not available + * + * @return null|float + */ + public function getMaximumValue() + { + return $this->maxValue; + } + + /** + * Return this performance data as string + * + * @return string + */ + public function __toString() + { + return $this->formatLabel(); + } + + /** + * Parse the current performance data value + * + * @todo Handle optional min/max if UOM == % + */ + protected function parse() + { + $parts = explode(';', $this->perfdataValue); + + $matches = array(); + if (preg_match('@^(-?(?:\d+)?(?:\.\d+)?)([a-zA-Z%°]{1,3})$@u', $parts[0], $matches)) { + $this->unit = strtolower($matches[2]); + $this->value = self::convert($matches[1], $this->unit); + } else { + $this->value = self::convert($parts[0]); + } + + switch (count($parts)) { + /* @noinspection PhpMissingBreakStatementInspection */ + case 5: + if ($parts[4] !== '') { + $this->maxValue = self::convert($parts[4], $this->unit); + } + /* @noinspection PhpMissingBreakStatementInspection */ + case 4: + if ($parts[3] !== '') { + $this->minValue = self::convert($parts[3], $this->unit); + } + /* @noinspection PhpMissingBreakStatementInspection */ + case 3: + $this->criticalThreshold = self::convert( + ThresholdRange::fromString(trim($parts[2])), + $this->unit + ); + // Fallthrough + case 2: + $this->warningThreshold = self::convert( + ThresholdRange::fromString(trim($parts[1])), + $this->unit + ); + } + + if ($this->warningThreshold === null) { + $this->warningThreshold = new ThresholdRange(); + } + if ($this->criticalThreshold === null) { + $this->criticalThreshold = new ThresholdRange(); + } + } + + /** + * Return the given value converted to its smallest supported representation + * + * @param string $value The value to convert + * @param string $fromUnit The unit the value currently represents + * + * @return null|float Null in case the value is not a number + */ + protected static function convert($value, $fromUnit = null) + { + if ($value instanceof ThresholdRange) { + $value = clone $value; + + $min = $value->getMin(); + if ($min !== null) { + $value->setMin(self::convert($min, $fromUnit)); + } + + $max = $value->getMax(); + if ($max !== null) { + $value->setMax(self::convert($max, $fromUnit)); + } + + return $value; + } + + if (is_numeric($value)) { + switch ($fromUnit) { + case 'us': + return $value / pow(10, 6); + case 'ms': + return $value / pow(10, 3); + case 'tb': + return floatval($value) * pow(2, 40); + case 'gb': + return floatval($value) * pow(2, 30); + case 'mb': + return floatval($value) * pow(2, 20); + case 'kb': + return floatval($value) * pow(2, 10); + default: + return (float) $value; + } + } + } + + protected function calculatePieChartData() + { + $rawValue = $this->getValue(); + $minValue = $this->getMinimumValue() !== null ? $this->getMinimumValue() : 0; + $usedValue = ($rawValue - $minValue); + + $green = $orange = $red = 0; + + if ($this->criticalThreshold->contains($rawValue)) { + if ($this->warningThreshold->contains($rawValue)) { + $green = $usedValue; + } else { + $orange = $usedValue; + } + } else { + $red = $usedValue; + } + + return array($green, $orange, $red, ($this->getMaximumValue() - $minValue) - $usedValue); + } + + + public function asInlinePie() + { + if (! $this->isVisualizable()) { + throw new ProgrammingError('Cannot calculate piechart data for unvisualizable perfdata entry.'); + } + + $data = $this->calculatePieChartData(); + $pieChart = new InlinePie($data, $this); + $pieChart->setColors(array('#44bb77', '#ffaa44', '#ff5566', '#ddccdd')); + + return $pieChart; + } + + /** + * Format the given value depending on the currently used unit + */ + protected function format($value) + { + if ($value === null) { + return null; + } + + if ($value instanceof ThresholdRange) { + if ($value->getMin()) { + return (string) $value; + } + + $max = $value->getMax(); + return $max === null ? '' : $this->format($max); + } + + if ($this->isPercentage()) { + return (string)$value . '%'; + } + if ($this->isBytes()) { + return Format::bytes($value); + } + if ($this->isSeconds()) { + return Format::seconds($value); + } + if ($this->isTemperature()) { + return (string)$value . strtoupper($this->unit); + } + return number_format($value, 2) . ($this->unit !== null ? ' ' . $this->unit : ''); + } + + /** + * Format the title string that represents this perfdata set + * + * @param bool $html + * + * @return string + */ + public function formatLabel($html = false) + { + return sprintf( + $html ? '<b>%s %s</b> (%s%%)' : '%s %s (%s%%)', + htmlspecialchars($this->getLabel()), + $this->format($this->value), + number_format($this->getPercentage() ?? 0, 2) + ); + } + + public function toArray() + { + return array( + 'label' => $this->getLabel(), + 'value' => $this->format($this->getvalue()), + 'min' => isset($this->minValue) && !$this->isPercentage() + ? $this->format($this->minValue) + : '', + 'max' => isset($this->maxValue) && !$this->isPercentage() + ? $this->format($this->maxValue) + : '', + 'warn' => $this->format($this->warningThreshold), + 'crit' => $this->format($this->criticalThreshold) + ); + } + + /** + * Return the state indicated by this perfdata + * + * @see Service + * + * @return int + */ + public function getState() + { + if ($this->value === null) { + return Service::STATE_UNKNOWN; + } + + if (! $this->criticalThreshold->contains($this->value)) { + return Service::STATE_CRITICAL; + } + + if (! $this->warningThreshold->contains($this->value)) { + return Service::STATE_WARNING; + } + + return Service::STATE_OK; + } + + /** + * Return whether the state indicated by this perfdata is worse than + * the state indicated by the other perfdata + * CRITICAL > UNKNOWN > WARNING > OK + * + * @param Perfdata $rhs the other perfdata + * + * @return bool + */ + public function worseThan(Perfdata $rhs) + { + if (($state = $this->getState()) === ($rhsState = $rhs->getState())) { + return $this->getPercentage() > $rhs->getPercentage(); + } + + if ($state === Service::STATE_CRITICAL) { + return true; + } + + if ($state === Service::STATE_UNKNOWN) { + return $rhsState !== Service::STATE_CRITICAL; + } + + if ($state === Service::STATE_WARNING) { + return $rhsState === Service::STATE_OK; + } + + return false; + } +} diff --git a/modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php b/modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php new file mode 100644 index 0000000..ef1ca0c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php @@ -0,0 +1,144 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Plugin; + +use ArrayIterator; +use IteratorAggregate; +use Traversable; + +class PerfdataSet implements IteratorAggregate +{ + /** + * The performance data being parsed + * + * @var string + */ + protected $perfdataStr; + + /** + * The current parsing position + * + * @var int + */ + protected $parserPos = 0; + + /** + * A list of Perfdata objects + * + * @var array + */ + protected $perfdata = array(); + + /** + * Create a new set of performance data + * + * @param string $perfdataStr A space separated list of label/value pairs + */ + protected function __construct($perfdataStr) + { + if ($perfdataStr && ($perfdataStr = trim($perfdataStr))) { + $this->perfdataStr = $perfdataStr; + $this->parse(); + } + } + + /** + * Return a iterator for this set of performance data + * + * @return ArrayIterator + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->asArray()); + } + + /** + * Return a new set of performance data + * + * @param string $perfdataStr A space separated list of label/value pairs + * + * @return PerfdataSet + */ + public static function fromString($perfdataStr) + { + return new static($perfdataStr); + } + + /** + * Return this set of performance data as array + * + * @return array + */ + public function asArray() + { + return $this->perfdata; + } + + /** + * Parse the current performance data + */ + protected function parse() + { + while ($this->parserPos < strlen($this->perfdataStr)) { + $label = trim($this->readLabel()); + $value = trim($this->readUntil(' ')); + + if ($label) { + $this->perfdata[] = new Perfdata($label, $value); + } + } + } + + /** + * Return the next label found in the performance data + * + * @return string The label found + */ + protected function readLabel() + { + $this->skipSpaces(); + if (in_array($this->perfdataStr[$this->parserPos], array('"', "'"))) { + $quoteChar = $this->perfdataStr[$this->parserPos++]; + $label = $this->readUntil('='); + $this->parserPos++; + + if (($closingPos = strpos($label, $quoteChar)) > 0) { + $label = substr($label, 0, $closingPos); + } + } else { + $label = $this->readUntil('='); + $this->parserPos++; + } + + $this->skipSpaces(); + return $label; + } + + /** + * Return all characters between the current parser position and the given character + * + * @param string $stopChar The character on which to stop + * + * @return string + */ + protected function readUntil($stopChar) + { + $start = $this->parserPos; + while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] !== $stopChar) { + $this->parserPos++; + } + + return substr($this->perfdataStr, $start, $this->parserPos - $start); + } + + /** + * Advance the parser position to the next non-whitespace character + */ + protected function skipSpaces() + { + while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] === ' ') { + $this->parserPos++; + } + } +} diff --git a/modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php b/modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php new file mode 100644 index 0000000..bd27b8b --- /dev/null +++ b/modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php @@ -0,0 +1,179 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Plugin; + +/** + * The warning/critical threshold of a measured value + */ +class ThresholdRange +{ + /** + * The smallest value inside the range (null stands for -∞) + * + * @var float|null + */ + protected $min; + + /** + * The biggest value inside the range (null stands for ∞) + * + * @var float|null + */ + protected $max; + + /** + * Whether to invert the result of contains() + * + * @var bool + */ + protected $inverted = false; + + /** + * The unmodified range as passed to fromString() + * + * @var string + */ + protected $raw; + + /** + * Create a new instance based on a threshold range conforming to <https://nagios-plugins.org/doc/guidelines.html> + * + * @param string $rawRange + * + * @return ThresholdRange + */ + public static function fromString($rawRange) + { + $range = new static(); + $range->raw = $rawRange; + + if ($rawRange == '') { + return $range; + } + + $rawRange = ltrim($rawRange); + if (substr($rawRange, 0, 1) === '@') { + $range->setInverted(); + $rawRange = substr($rawRange, 1); + } + + if (strpos($rawRange, ':') === false) { + $min = 0.0; + $max = floatval(trim($rawRange)); + } else { + list($min, $max) = explode(':', $rawRange, 2); + $min = trim($min); + $max = trim($max); + + switch ($min) { + case '': + $min = 0.0; + break; + case '~': + $min = null; + break; + default: + $min = floatval($min); + } + + $max = empty($max) ? null : floatval($max); + } + + return $range->setMin($min) + ->setMax($max); + } + + /** + * Set the smallest value inside the range (null stands for -∞) + * + * @param float|null $min + * + * @return $this + */ + public function setMin($min) + { + $this->min = $min; + return $this; + } + + /** + * Get the smallest value inside the range (null stands for -∞) + * + * @return float|null + */ + public function getMin() + { + return $this->min; + } + + /** + * Set the biggest value inside the range (null stands for ∞) + * + * @param float|null $max + * + * @return $this + */ + public function setMax($max) + { + $this->max = $max; + return $this; + } + + /** + * Get the biggest value inside the range (null stands for ∞) + * + * @return float|null + */ + public function getMax() + { + return $this->max; + } + + /** + * Set whether to invert the result of contains() + * + * @param bool $inverted + * + * @return $this + */ + public function setInverted($inverted = true) + { + $this->inverted = $inverted; + return $this; + } + + /** + * Get whether to invert the result of contains() + * + * @return bool + */ + public function isInverted() + { + return $this->inverted; + } + + /** + * Return whether $value is inside $this + * + * @param float $value + * + * @return bool + */ + public function contains($value) + { + return (bool) ($this->inverted ^ ( + ($this->min === null || $this->min <= $value) && ($this->max === null || $this->max >= $value) + )); + } + + /** + * Return the textual representation of $this, suitable for fromString() + * + * @return string + */ + public function __toString() + { + return (string) $this->raw; + } +} diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php b/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php new file mode 100644 index 0000000..4e2e61c --- /dev/null +++ b/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php @@ -0,0 +1,32 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\ProvidedHook; + +use Icinga\Application\Hook\ApplicationStateHook; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; + +class ApplicationState extends ApplicationStateHook +{ + public function collectMessages() + { + $backend = MonitoringBackend::instance(); + + $programStatus = $backend + ->select() + ->from( + 'programstatus', + ['is_currently_running', 'status_update_time'] + ) + ->fetchRow(); + + if ($programStatus === false || ! (bool) $programStatus->is_currently_running) { + $message = sprintf( + mt('monitoring', "Monitoring backend '%s' is not running."), + $backend->getName() + ); + + $this->addError('monitoring/backend-down', $programStatus->status_update_time, $message); + } + } +} diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/Health.php b/modules/monitoring/library/Monitoring/ProvidedHook/Health.php new file mode 100644 index 0000000..8f9c893 --- /dev/null +++ b/modules/monitoring/library/Monitoring/ProvidedHook/Health.php @@ -0,0 +1,102 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\ProvidedHook; + +use Icinga\Application\Hook\HealthHook; +use Icinga\Date\DateFormatter; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use ipl\Web\Url; + +class Health extends HealthHook +{ + /** @var object */ + protected $programStatus; + + public function getName() + { + return 'Icinga'; + } + + public function getUrl() + { + return Url::fromPath('monitoring/health/info'); + } + + public function checkHealth() + { + $backendName = MonitoringBackend::instance()->getName(); + $programStatus = $this->getProgramStatus(); + if ($programStatus === false) { + $this->setState(self::STATE_UNKNOWN); + $this->setMessage(sprintf(t('%s is currently not up and running'), $backendName)); + return; + } + + if ($programStatus->is_currently_running) { + $this->setState(self::STATE_OK); + $this->setMessage(sprintf( + t( + '%1$s has been up and running with PID %2$d %3$s', + 'Last format parameter represents the time running' + ), + $backendName, + $programStatus->process_id, + DateFormatter::timeSince($programStatus->program_start_time) + )); + + $warningMessages = []; + + if (! $programStatus->active_host_checks_enabled) { + $this->setState(self::STATE_WARNING); + $warningMessages[] = t('Active host checks are disabled'); + } + + if (! $programStatus->active_service_checks_enabled) { + $this->setState(self::STATE_WARNING); + $warningMessages[] = t('Active service checks are disabled'); + } + + if (! $programStatus->notifications_enabled) { + $this->setState(self::STATE_WARNING); + $warningMessages[] = t('Notifications are disabled'); + } + + if ($this->getState() === self::STATE_WARNING) { + $this->setMessage(implode("; ", $warningMessages)); + } + } else { + $this->setState(self::STATE_CRITICAL); + $this->setMessage(sprintf(t('Backend %s is not running'), $backendName)); + } + + $this->setMetrics((array) $programStatus); + } + + protected function getProgramStatus() + { + if ($this->programStatus === null) { + $this->programStatus = MonitoringBackend::instance()->select() + ->from('programstatus', [ + 'program_version', + 'status_update_time', + 'program_start_time', + 'program_end_time', + 'endpoint_name', + 'is_currently_running', + 'process_id', + 'last_command_check', + 'last_log_rotation', + 'notifications_enabled', + 'active_service_checks_enabled', + 'active_host_checks_enabled', + 'event_handlers_enabled', + 'flap_detection_enabled', + 'process_performance_data' + ]) + ->fetchRow(); + } + + return $this->programStatus; + } +} diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php b/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php new file mode 100644 index 0000000..fd1818f --- /dev/null +++ b/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php @@ -0,0 +1,35 @@ +<?php +/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\ProvidedHook\X509; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Module\X509\Hook\SniHook; + +class Sni extends SniHook +{ + public function getHosts(Filter $filter = null) + { + $hosts = MonitoringBackend::instance() + ->select() + ->from('hoststatus', [ + 'host_name', + 'host_address', + 'host_address6' + ]); + if ($filter !== null) { + $hosts->applyFilter($filter); + } + + foreach ($hosts as $host) { + if (! empty($host->host_address)) { + yield $host->host_address => $host->host_name; + } + + if (! empty($host->host_address6)) { + yield $host->host_address6 => $host->host_name; + } + } + } +} diff --git a/modules/monitoring/library/Monitoring/SecurityStep.php b/modules/monitoring/library/Monitoring/SecurityStep.php new file mode 100644 index 0000000..94053b3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/SecurityStep.php @@ -0,0 +1,84 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring; + +use Exception; +use Icinga\Module\Setup\Step; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; + +class SecurityStep extends Step +{ + protected $data; + + protected $error; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $config = array(); + $config['security'] = $this->data['securityConfig']; + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('modules/monitoring/config.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '<h2>' . mt('monitoring', 'Monitoring Security', 'setup.page.title') . '</h2>'; + $pageDescription = '<p>' . mt( + 'monitoring', + 'Icinga Web 2 will protect your monitoring environment against' + . ' prying eyes using the configuration specified below:' + ) . '</p>'; + + $pageHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Protected Custom Variables') . '</strong></td>' + . '<td>' . ($this->data['securityConfig']['protected_customvars'] ? ( + $this->data['securityConfig']['protected_customvars'] + ) : mt('monitoring', 'None', 'monitoring.protected_customvars')) . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + + return $pageTitle . '<div class="topic">' . $pageDescription . $pageHtml . '</div>'; + } + + public function getReport() + { + if ($this->error === false) { + return array(sprintf( + mt('monitoring', 'Monitoring security configuration has been successfully created: %s'), + Config::resolvePath('modules/monitoring/config.ini') + )); + } elseif ($this->error !== null) { + return array( + sprintf( + mt( + 'monitoring', + 'Monitoring security configuration could not be written to: %s. An error occured:' + ), + Config::resolvePath('modules/monitoring/config.ini') + ), + sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) + ); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php new file mode 100644 index 0000000..ee313b3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php @@ -0,0 +1,233 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Timeline; + +use DateTime; +use Icinga\Web\Url; +use Icinga\Exception\ProgrammingError; + +/** + * An event group that is part of a timeline + */ +class TimeEntry +{ + /** + * The name of this group + * + * @var string + */ + protected $name; + + /** + * The amount of events that are part of this group + * + * @var int + */ + protected $value; + + /** + * The date and time of this group + * + * @var DateTime + */ + protected $dateTime; + + /** + * The url to this group's detail view + * + * @var Url + */ + protected $detailUrl; + + /** + * The weight of this group + * + * @var float + */ + protected $weight = 1.0; + + /** + * The label of this group + * + * @var string + */ + protected $label; + + /** + * The CSS class of the entry + * + * @var string + */ + protected $class; + + /** + * Return a new TimeEntry object with the given attributes being set + * + * @param array $attributes The attributes to set + * @return TimeEntry The resulting TimeEntry object + * @throws ProgrammingError If one of the given attributes cannot be set + */ + public static function fromArray(array $attributes) + { + $entry = new TimeEntry(); + + foreach ($attributes as $name => $value) { + $methodName = 'set' . ucfirst($name); + if (method_exists($entry, $methodName)) { + $entry->{$methodName}($value); + } else { + throw new ProgrammingError( + 'Method "%s" does not exist on object of type "%s"', + $methodName, + __CLASS__ + ); + } + } + + return $entry; + } + + /** + * Set this group's name + * + * @param string $name The name to set + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Return the name of this group + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set this group's amount of events + * + * @param int $value The value to set + */ + public function setValue($value) + { + $this->value = intval($value); + } + + /** + * Return the amount of events in this group + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * Set this group's date and time + * + * @param DateTime $dateTime The date and time to set + */ + public function setDateTime(DateTime $dateTime) + { + $this->dateTime = $dateTime; + } + + /** + * Return the date and time of this group + * + * @return DateTime + */ + public function getDateTime() + { + return $this->dateTime; + } + + /** + * Set the url to this group's detail view + * + * @param Url $detailUrl The url to set + */ + public function setDetailUrl(Url $detailUrl) + { + $this->detailUrl = $detailUrl; + } + + /** + * Return the url to this group's detail view + * + * @return Url + */ + public function getDetailUrl() + { + return $this->detailUrl; + } + + /** + * Set this group's weight + * + * @param float $weight The weight for this group + */ + public function setWeight($weight) + { + $this->weight = floatval($weight); + } + + /** + * Return the weight of this group + * + * @return float + */ + public function getWeight() + { + return $this->weight; + } + + /** + * Set this group's label + * + * @param string $label The label to set + */ + public function setLabel($label) + { + $this->label = $label; + } + + /** + * Return the label of this group + * + * @return string + */ + public function getLabel() + { + return $this->label; + } + + /** + * Get the CSS class + * + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * Set the CSS class + * + * @param string $class + * + * @return $this + */ + public function setClass($class) + { + $this->class = $class; + return $this; + } +} diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeLine.php b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php new file mode 100644 index 0000000..128b64b --- /dev/null +++ b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php @@ -0,0 +1,491 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Timeline; + +use DateTime; +use Exception; +use ArrayIterator; +use Icinga\Exception\IcingaException; +use IteratorAggregate; +use Icinga\Data\Filter\Filter; +use Icinga\Web\Hook; +use Icinga\Web\Session\SessionNamespace; +use Icinga\Module\Monitoring\DataView\DataView; +use Traversable; + +/** + * Represents a set of events in a specific range of time + */ +class TimeLine implements IteratorAggregate +{ + /** + * The resultset returned by the dataview + * + * @var array + */ + private $resultset; + + /** + * The groups this timeline uses for display purposes + * + * @var array + */ + private $displayGroups; + + /** + * The session to use + * + * @var SessionNamespace + */ + protected $session; + + /** + * The base that is used to calculate each circle's diameter + * + * @var float + */ + protected $calculationBase; + + /** + * The dataview to fetch entries from + * + * @var DataView + */ + protected $dataview; + + /** + * The names by which to group entries + * + * @var array + */ + protected $identifiers; + + /** + * The range of time for which to display entries + * + * @var TimeRange + */ + protected $displayRange; + + /** + * The range of time for which to calculate forecasts + * + * @var TimeRange + */ + protected $forecastRange; + + /** + * The maximum diameter each circle can have + * + * @var float + */ + protected $circleDiameter = 100.0; + + /** + * The minimum diameter each circle can have + * + * @var float + */ + protected $minCircleDiameter = 1.0; + + /** + * The unit of a circle's diameter + * + * @var string + */ + protected $diameterUnit = 'px'; + + /** + * Return a iterator for this timeline + * + * @return ArrayIterator + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->toArray()); + } + + /** + * Create a new timeline + * + * The given dataview must provide the following columns: + * - name A string identifying an entry (Corresponds to the keys of "$identifiers") + * - time A unix timestamp that defines where to place an entry on the timeline + * + * @param DataView $dataview The dataview to fetch entries from + * @param array $identifiers The names by which to group entries + */ + public function __construct(DataView $dataview, array $identifiers) + { + $this->dataview = $dataview; + $this->identifiers = $identifiers; + } + + /** + * Set the session to use + * + * @param SessionNamespace $session The session to use + */ + public function setSession(SessionNamespace $session) + { + $this->session = $session; + } + + /** + * Set the range of time for which to display elements + * + * @param TimeRange $range The range of time for which to display elements + */ + public function setDisplayRange(TimeRange $range) + { + $this->displayRange = $range; + } + + /** + * Set the range of time for which to calculate forecasts + * + * @param TimeRange $range The range of time for which to calculate forecasts + */ + public function setForecastRange(TimeRange $range) + { + $this->forecastRange = $range; + } + + /** + * Set the maximum diameter each circle can have + * + * @param string $width The diameter to set, suffixed with its unit + * + * @throws Exception If the given diameter is invalid + */ + public function setMaximumCircleWidth($width) + { + $matches = array(); + if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) { + $this->circleDiameter = floatval($matches[1]); + $this->diameterUnit = $matches[2]; + } else { + throw new IcingaException( + 'Width "%s" is not a valid width', + $width + ); + } + } + + /** + * Set the minimum diameter each circle can have + * + * @param string $width The diameter to set, suffixed with its unit + * + * @throws Exception If the given diameter is invalid or its unit differs from the maximum + */ + public function setMinimumCircleWidth($width) + { + $matches = array(); + if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) { + if ($matches[2] === $this->diameterUnit) { + $this->minCircleDiameter = floatval($matches[1]); + } else { + throw new IcingaException( + 'Unit needs to be in "%s"', + $this->diameterUnit + ); + } + } else { + throw new IcingaException( + 'Width "%s" is not a valid width', + $width + ); + } + } + + /** + * Return all known group types (identifiers) with their respective labels and classess as array + * + * @return array + */ + public function getGroupInfo() + { + $groupInfo = array(); + foreach ($this->identifiers as $name => $attributes) { + if (isset($attributes['groupBy'])) { + $name = $attributes['groupBy']; + } + + $groupInfo[$name]['class'] = $attributes['class']; + $groupInfo[$name]['label'] = $attributes['label']; + } + + return $groupInfo; + } + + /** + * Return the circle's diameter for the given event group + * + * @param TimeEntry $group The group for which to return a circle width + * @param int $precision Amount of decimal places to preserve + * + * @return string + */ + public function calculateCircleWidth(TimeEntry $group, $precision = 0) + { + $base = $this->getCalculationBase(true); + $factor = log($group->getValue() * $group->getWeight(), $base) / 100; + $width = $this->circleDiameter * $factor; + return sprintf( + '%.' . $precision . 'F%s', + $width > $this->minCircleDiameter ? $width : $this->minCircleDiameter, + $this->diameterUnit + ); + } + + /** + * Return an extrapolated circle width for the given event group + * + * @param TimeEntry $group The event group for which to return an extrapolated circle width + * @param int $precision Amount of decimal places to preserve + * + * @return string + */ + public function getExtrapolatedCircleWidth(TimeEntry $group, $precision = 0) + { + $eventCount = 0; + foreach ($this->displayGroups as $groups) { + if (array_key_exists($group->getName(), $groups)) { + $eventCount += $groups[$group->getName()]->getValue(); + } + } + + $extrapolatedCount = (int) $eventCount / count($this->displayGroups); + if ($extrapolatedCount < $group->getValue()) { + return $this->calculateCircleWidth($group, $precision); + } + + return $this->calculateCircleWidth( + TimeEntry::fromArray( + array( + 'value' => $extrapolatedCount, + 'weight' => $group->getWeight() + ) + ), + $precision + ); + } + + /** + * Return the base that should be used to calculate circle widths + * + * @param bool $create Whether to generate a new base if none is known yet + * + * @return float|null + */ + public function getCalculationBase($create) + { + if ($this->calculationBase === null) { + $calculationBase = $this->session !== null ? $this->session->get('calculationBase') : null; + + if ($create) { + $new = $this->generateCalculationBase(); + if ($new > $calculationBase) { + $this->calculationBase = $new; + + if ($this->session !== null) { + $this->session->calculationBase = $new; + } + } else { + $this->calculationBase = $calculationBase; + } + } else { + return $calculationBase; + } + } + + return $this->calculationBase; + } + + /** + * Generate a new base to calculate circle widths with + * + * @return float + */ + protected function generateCalculationBase() + { + $allEntries = $this->groupEntries( + array_merge( + $this->fetchEntries(), + $this->fetchForecasts() + ), + new TimeRange( + $this->displayRange->getStart(), + $this->forecastRange->getEnd(), + $this->displayRange->getInterval() + ) + ); + + $highestValue = 0; + foreach ($allEntries as $groups) { + foreach ($groups as $group) { + if ($group->getValue() * $group->getWeight() > $highestValue) { + $highestValue = $group->getValue() * $group->getWeight(); + } + } + } + + return pow($highestValue, 1 / 100); // 100 == 100% + } + + /** + * Fetch all entries and forecasts by using the dataview associated with this timeline + * + * @return array The dataview's result + */ + private function fetchResults() + { + $hookResults = array(); + foreach (Hook::all('timeline') as $timelineProvider) { + $hookResults = array_merge( + $hookResults, + $timelineProvider->fetchEntries($this->displayRange), + $timelineProvider->fetchForecasts($this->forecastRange) + ); + + foreach ($timelineProvider->getIdentifiers() as $identifier => $attributes) { + if (!array_key_exists($identifier, $this->identifiers)) { + $this->identifiers[$identifier] = $attributes; + } + } + } + + $query = $this->dataview; + $filter = Filter::matchAll( + Filter::where('type', array_keys($this->identifiers)), + Filter::expression('timestamp', '<=', $this->displayRange->getStart()->getTimestamp()), + Filter::expression('timestamp', '>', $this->displayRange->getEnd()->getTimestamp()) + ); + $query->applyFilter($filter); + return array_merge($query->getQuery()->fetchAll(), $hookResults); + } + + /** + * Fetch all entries + * + * @return array The entries to display on the timeline + */ + protected function fetchEntries() + { + if ($this->resultset === null) { + $this->resultset = $this->fetchResults(); + } + + $range = $this->displayRange; + return array_filter( + $this->resultset, + function ($e) use ($range) { + return $range->validateTime($e->time); + } + ); + } + + /** + * Fetch all forecasts + * + * @return array The entries to calculate forecasts with + */ + protected function fetchForecasts() + { + if ($this->resultset === null) { + $this->resultset = $this->fetchResults(); + } + + $range = $this->forecastRange; + return array_filter( + $this->resultset, + function ($e) use ($range) { + return $range->validateTime($e->time); + } + ); + } + + /** + * Return the given entries grouped together + * + * @param array $entries The entries to group + * @param TimeRange $timeRange The range of time to group by + * + * @return array displayGroups The grouped entries + */ + protected function groupEntries(array $entries, TimeRange $timeRange) + { + $counts = array(); + foreach ($entries as $entry) { + $entryTime = new DateTime(); + $entryTime->setTimestamp($entry->time); + $timestamp = $timeRange->findTimeframe($entryTime, true); + + if ($timestamp !== null) { + if (array_key_exists($entry->name, $counts)) { + if (array_key_exists($timestamp, $counts[$entry->name])) { + $counts[$entry->name][$timestamp] += 1; + } else { + $counts[$entry->name][$timestamp] = 1; + } + } else { + $counts[$entry->name][$timestamp] = 1; + } + } + } + + $groups = array(); + foreach ($counts as $name => $data) { + foreach ($data as $timestamp => $count) { + $dateTime = new DateTime(); + $dateTime->setTimestamp($timestamp); + + $groupName = $name; + if (isset($this->identifiers[$name]['groupBy'])) { + $groupName = $this->identifiers[$name]['groupBy']; + } + + if (isset($groups[$timestamp][$groupName])) { + $groups[$timestamp][$groupName]->setValue( + $groups[$timestamp][$groupName]->getValue() + $count + ); + } else { + $groups[$timestamp][$groupName] = TimeEntry::fromArray( + array( + 'name' => $groupName, + 'value' => $count, + 'dateTime' => $dateTime, + 'class' => $this->identifiers[$name]['class'], + 'detailUrl' => $this->identifiers[$name]['detailUrl'], + 'label' => $this->identifiers[$name]['label'] + ) + ); + } + } + } + + return $groups; + } + + /** + * Return the contents of this timeline as array + * + * @return array + */ + protected function toArray() + { + $this->displayGroups = $this->groupEntries($this->fetchEntries(), $this->displayRange); + + $array = array(); + foreach ($this->displayRange as $timestamp => $timeframe) { + $array[] = array( + $timeframe, + array_key_exists($timestamp, $this->displayGroups) ? $this->displayGroups[$timestamp] : array() + ); + } + + return $array; + } +} diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeRange.php b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php new file mode 100644 index 0000000..08c7a2c --- /dev/null +++ b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php @@ -0,0 +1,258 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Timeline; + +use StdClass; +use Iterator; +use DateTime; +use DateInterval; +use Icinga\Util\Format; + +/** + * A range of time split into a specific interval + * + * @see Iterator + */ +class TimeRange implements Iterator +{ + /** + * The start of this time range + * + * @var DateTime + */ + protected $start; + + /** + * The end of this time range + * + * @var DateTime + */ + protected $end; + + /** + * The interval by which this time range is split + * + * @var DateInterval + */ + protected $interval; + + /** + * The current date in the iteration + * + * @var DateTime + */ + protected $current; + + /** + * Whether the date iteration is negative + * + * @var bool + */ + protected $negative; + + /** + * Initialize a new time range + * + * @param DateTime $start When the time range should start + * @param DateTime $end When the time range should end + * @param DateInterval $interval The interval of the time range + */ + public function __construct(DateTime $start, DateTime $end, DateInterval $interval) + { + $this->interval = $interval; + $this->start = $start; + $this->end = $end; + $this->negative = $this->start > $this->end; + } + + /** + * Return when this range of time starts + * + * @return DateTime + */ + public function getStart() + { + return $this->start; + } + + /** + * Return when this range of time ends + * + * @return DateTime + */ + public function getEnd() + { + return $this->end; + } + + /** + * Return the interval by which this time range is split + * + * @return DateInterval + */ + public function getInterval() + { + return $this->interval; + } + + /** + * Return the appropriate timeframe for the given date and time or null if none could be found + * + * @param DateTime $dateTime The date and time for which to search the timeframe + * @param bool $asTimestamp Whether the start of the timeframe should be returned as timestamp + * @return StdClass|int An object with a ´start´ and ´end´ property or a timestamp + */ + public function findTimeframe(DateTime $dateTime, $asTimestamp = false) + { + foreach ($this as $timeframeIdentifier => $timeframe) { + if ($this->negative) { + if ($dateTime <= $timeframe->start && $dateTime >= $timeframe->end) { + return $asTimestamp ? $timeframeIdentifier : $timeframe; + } + } elseif ($dateTime >= $timeframe->start && $dateTime <= $timeframe->end) { + return $asTimestamp ? $timeframeIdentifier : $timeframe; + } + } + } + + /** + * Return whether the given time is within this range of time + * + * @param string|int|DateTime $time The timestamp or date and time to check + */ + public function validateTime($time) + { + if ($time instanceof DateTime) { + $dateTime = $time; + } elseif (is_string($time)) { + $dateTime = DateTime::createFromFormat('d/m/Y g:i A', $time); + } else { + $dateTime = new DateTime(); + $dateTime->setTimestamp($time); + } + + return ($this->negative && ($dateTime <= $this->start && $dateTime >= $this->end)) || + (!$this->negative && ($dateTime >= $this->start && $dateTime <= $this->end)); + } + + /** + * Return the appropriate timeframe for the given timeframe start + * + * @param int|DateTime $time The timestamp or date and time for which to return the timeframe + * @return StdClass An object with a ´start´ and ´end´ property + */ + public function getTimeframe($time) + { + if ($time instanceof DateTime) { + $startTime = clone $time; + } else { + $startTime = new DateTime(); + $startTime->setTimestamp($time); + } + + return $this->buildTimeframe($startTime, $this->applyInterval(clone $startTime, 1)); + } + + /** + * Apply the current interval to the given date and time + * + * @param DateTime $dateTime The date and time to apply the interval to + * @param int $adjustBy By how much seconds the resulting date and time should be adjusted + * + * @return DateTime + */ + protected function applyInterval(DateTime $dateTime, $adjustBy) + { + if (!$this->interval->y && !$this->interval->m) { + if ($this->negative) { + return $dateTime->sub($this->interval)->add(new DateInterval('PT' . $adjustBy . 'S')); + } else { + return $dateTime->add($this->interval)->sub(new DateInterval('PT' . $adjustBy . 'S')); + } + } elseif ($this->interval->m) { + for ($i = 0; $i < $this->interval->m; $i++) { + if ($this->negative) { + $dateTime->sub(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S')); + } else { + $dateTime->add(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S')); + } + } + } elseif ($this->interval->y) { + for ($i = 0; $i < $this->interval->y; $i++) { + if ($this->negative) { + $dateTime->sub(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S')); + } else { + $dateTime->add(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S')); + } + } + } + $adjustment = new DateInterval('PT' . $adjustBy . 'S'); + return $this->negative ? $dateTime->add($adjustment) : $dateTime->sub($adjustment); + } + + /** + * Return an object representation of the given timeframe + * + * @param DateTime $start The start of the timeframe + * @param DateTime $end The end of the timeframe + * @return StdClass + */ + protected function buildTimeframe(DateTime $start, DateTime $end) + { + $timeframe = new StdClass(); + $timeframe->start = $start; + $timeframe->end = $end; + return $timeframe; + } + + /** + * Reset the iterator to its initial state + */ + public function rewind(): void + { + $this->current = clone $this->start; + } + + /** + * Return whether the current iteration step is valid + * + * @return bool + */ + public function valid(): bool + { + if ($this->negative) { + return $this->current > $this->end; + } else { + return $this->current < $this->end; + } + } + + /** + * Return the current value in the iteration + * + * @return StdClass + */ + public function current(): object + { + return $this->getTimeframe($this->current); + } + + /** + * Return a unique identifier for the current value in the iteration + * + * @return int + */ + public function key(): int + { + return $this->current->getTimestamp(); + } + + /** + * Advance the iterator position by one + */ + public function next(): void + { + $this->applyInterval($this->current, 0); + } +} diff --git a/modules/monitoring/library/Monitoring/TransportStep.php b/modules/monitoring/library/Monitoring/TransportStep.php new file mode 100644 index 0000000..d138eb4 --- /dev/null +++ b/modules/monitoring/library/Monitoring/TransportStep.php @@ -0,0 +1,143 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring; + +use Exception; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Setup\Step; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; + +class TransportStep extends Step +{ + protected $data; + + protected $error; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $transportConfig = $this->data['transportConfig']; + $transportName = $transportConfig['name']; + unset($transportConfig['name']); + + try { + Config::fromArray(array($transportName => $transportConfig)) + ->setConfigFile(Config::resolvePath('modules/monitoring/commandtransports.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + switch ($this->data['transportConfig']['transport']) { + case 'local': + $details = '<p>' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will use the named pipe located at "%s"' + . ' to send commands to your monitoring instance.' + ), + $this->data['transportConfig']['path'] + ) . '</p>'; + break; + case 'remote': + $details = '<p>' + . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will use the named pipe located on a remote machine at "%s" to send commands' + . ' to your monitoring instance by using the connection details listed below:' + ), + $this->data['transportConfig']['path'] + ) + . '</p>' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Remote Host') . '</strong></td>' + . '<td>' . $this->data['transportConfig']['host'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Remote SSH Port') . '</strong></td>' + . '<td>' . $this->data['transportConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Remote SSH User') . '</strong></td>' + . '<td>' . $this->data['transportConfig']['user'] . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + break; + case 'api': + $details = '<p>' + . mt( + 'monitoring', + 'Icinga Web 2 will use the Icinga 2 API to send commands' + . ' to your monitoring instance by using the connection details listed below:' + ) + . '</p>' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Host') . '</strong></td>' + . '<td>' . $this->data['transportConfig']['host'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Port') . '</strong></td>' + . '<td>' . $this->data['transportConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Username') . '</strong></td>' + . '<td>' . $this->data['transportConfig']['username'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('monitoring', 'Password') . '</strong></td>' + . '<td>' . str_repeat('*', strlen($this->data['transportConfig']['password'])) . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + break; + default: + throw new ProgrammingError( + 'Unknown command transport type: %s', + $this->data['transportConfig']['transport'] + ); + } + + return '<h2>' . mt('monitoring', 'Command Transport', 'setup.page.title') . '</h2>' + . '<div class="topic">' . $details . '</div>'; + } + + public function getReport() + { + if ($this->error === false) { + return array(sprintf( + mt('monitoring', 'Command transport configuration has been successfully created: %s'), + Config::resolvePath('modules/monitoring/commandtransports.ini') + )); + } elseif ($this->error !== null) { + return array( + sprintf( + mt( + 'monitoring', + 'Command transport configuration could not be written to: %s. An error occured:' + ), + Config::resolvePath('modules/monitoring/commandtransports.ini') + ), + sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) + ); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php new file mode 100644 index 0000000..014ac43 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php @@ -0,0 +1,337 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Controller; + +use Exception; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm; +use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm; +use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; +use Icinga\Module\Monitoring\Hook\ObjectDetailsTabHook; +use Icinga\Web\Hook; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; + +/** + * Base class for the host and service controller + */ +abstract class MonitoredObjectController extends Controller +{ + /** + * The requested host or service + * + * @var \Icinga\Module\Monitoring\Object\Host|\Icinga\Module\Monitoring\Object\Host + */ + protected $object; + + /** + * URL to redirect to after a command was handled + * + * @var string + */ + protected $commandRedirectUrl; + + /** + * List of visible hooked tabs + * + * @var ObjectDetailsTabHook[] + */ + protected $tabHooks = []; + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Controller\ActionController For the method documentation. + */ + public function prepareInit() + { + parent::prepareInit(); + if (Hook::has('ticket')) { + $this->view->tickets = Hook::first('ticket'); + } + if (Hook::has('grapher')) { + $this->view->graphers = Hook::all('grapher'); + } + } + + /** + * Show a host or service + */ + public function showAction() + { + $this->setAutorefreshInterval(10); + $this->setupQuickActionForms(); + $auth = $this->Auth(); + $this->object->populate(); + $this->handleFormatRequest(); + $toggleFeaturesForm = new ToggleObjectFeaturesCommandForm(array( + 'backend' => $this->backend, + 'objects' => $this->object + )); + $toggleFeaturesForm + ->load($this->object) + ->handleRequest(); + $this->view->toggleFeaturesForm = $toggleFeaturesForm; + if (! empty($this->object->comments) && $auth->hasPermission('monitoring/command/comment/delete')) { + $delCommentForm = new DeleteCommentCommandForm(); + $delCommentForm->handleRequest(); + $this->view->delCommentForm = $delCommentForm; + } + if (! empty($this->object->downtimes) && $auth->hasPermission('monitoring/command/downtime/delete')) { + $delDowntimeForm = new DeleteDowntimeCommandForm(); + $delDowntimeForm->handleRequest(); + $this->view->delDowntimeForm = $delDowntimeForm; + } + $this->view->showInstance = $this->backend->select()->from('instance')->count() > 1; + $this->view->object = $this->object; + + $this->view->extensionsHtml = array(); + foreach (Hook::all('Monitoring\DetailviewExtension') as $hook) { + /** @var DetailviewExtensionHook $hook */ + + try { + $html = $hook->setView($this->view)->getHtmlForObject($this->object); + } catch (Exception $e) { + $html = $this->view->escape($e->getMessage()); + } + + if ($html) { + $module = $this->view->escape($hook->getModule()->getName()); + $this->view->extensionsHtml[] = + '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">' + . $html + . '</div>'; + } + } + } + + /** + * Show the history for a host or service + */ + public function historyAction() + { + $this->getTabs()->activate('history'); + $this->view->history = $this->object->fetchEventHistory()->eventhistory; + $this->applyRestriction('monitoring/filter/objects', $this->view->history); + + $this->setupLimitControl(50); + $this->setupPaginationControl($this->view->history, 50); + $this->view->object = $this->object; + $this->render('object/detail-history', null, true); + } + + /** + * Show the content of a custom tab + */ + public function tabhookAction() + { + $hookName = $this->params->get('hook'); + $this->getTabs()->activate($hookName); + + $hook = $this->tabHooks[$hookName]; + + $this->view->header = $hook->getHeader($this->object, $this->getRequest()); + $this->view->content = $hook->getContent($this->object, $this->getRequest()); + $this->view->object = $this->object; + $this->render('object/detail-tabhook', null, true); + } + + /** + * Handle a command form + * + * @param ObjectsCommandForm $form + * + * @return ObjectsCommandForm + */ + protected function handleCommandForm(ObjectsCommandForm $form) + { + $form + ->setBackend($this->backend) + ->setObjects($this->object) + ->setRedirectUrl(Url::fromPath($this->commandRedirectUrl)->setParams($this->params)) + ->handleRequest(); + $this->view->form = $form; + $this->view->object = $this->object; + $this->view->tabs->remove('dashboard'); + $this->view->tabs->remove('menu-entry'); + $this->_helper->viewRenderer('partials/command/object-command-form', null, true); + $this->setupQuickActionForms(); + return $form; + } + + /** + * Export to JSON if requested + */ + protected function handleFormatRequest($query = null) + { + if ($this->params->get('format') === 'json' + || $this->getRequest()->getHeader('Accept') === 'application/json' + ) { + $payload = (array) $this->object->properties; + $payload['vars'] = $this->object->customvars; + + if ($this->hasPermission('*') || ! $this->hasPermission('no-monitoring/contacts')) { + $payload['contacts'] = $this->object->contacts->fetchPairs(); + $payload['contact_groups'] = $this->object->contactgroups->fetchPairs(); + } else { + $payload['contacts'] = []; + $payload['contact_groups'] = []; + } + + $groupName = $this->object->getType() . 'groups'; + $payload[$groupName] = $this->object->$groupName; + $this->getResponse()->json() + ->setSuccessData($payload) + ->setAutoSanitize() + ->sendResponse(); + } + } + + /** + * Acknowledge a problem + */ + abstract public function acknowledgeProblemAction(); + + /** + * Add a comment + */ + abstract public function addCommentAction(); + + /** + * Reschedule a check + */ + abstract public function rescheduleCheckAction(); + + /** + * Schedule a downtime + */ + abstract public function scheduleDowntimeAction(); + + /** + * Create tabs + */ + protected function createTabs() + { + $tabs = $this->getTabs(); + $object = $this->object; + if ($object->getType() === $object::TYPE_HOST) { + $isService = false; + $params = array( + 'host' => $object->getName() + ); + if ($this->params->has('service')) { + $params['service'] = $this->params->get('service'); + } + } else { + $isService = true; + $params = array( + 'host' => $object->getHost()->getName(), + 'service' => $object->getName() + ); + } + $tabs->add( + 'host', + array( + 'title' => sprintf( + $this->translate('Show detailed information for host %s'), + $isService ? $object->getHost()->getName() : $object->getName() + ), + 'label' => $this->translate('Host'), + 'url' => 'monitoring/host/show', + 'urlParams' => $params + ) + ); + if ($isService || $this->params->has('service')) { + $tabs->add( + 'service', + array( + 'title' => sprintf( + $this->translate('Show detailed information for service %s on host %s'), + $isService ? $object->getName() : $this->params->get('service'), + $isService ? $object->getHost()->getName() : $object->getName() + ), + 'label' => $this->translate('Service'), + 'url' => 'monitoring/service/show', + 'urlParams' => $params + ) + ); + } + $tabs->add( + 'services', + array( + 'title' => sprintf( + $this->translate('List all services on host %s'), + $isService ? $object->getHost()->getName() : $object->getName() + ), + 'label' => $this->translate('Services'), + 'url' => 'monitoring/host/services', + 'urlParams' => $params + ) + ); + if ($this->backend->hasQuery('eventhistory')) { + $tabs->add( + 'history', + array( + 'title' => $isService + ? sprintf( + $this->translate('Show all event records of service %s on host %s'), + $object->getName(), + $object->getHost()->getName() + ) + : sprintf($this->translate('Show all event records of host %s'), $object->getName()) + , + 'label' => $this->translate('History'), + 'url' => $isService ? 'monitoring/service/history' : 'monitoring/host/history', + 'urlParams' => $params + ) + ); + } + + /** @var ObjectDetailsTabHook $hook */ + foreach (Hook::all('Monitoring\\ObjectDetailsTab') as $hook) { + $hookName = $hook->getName(); + if ($hook->shouldBeShown($object, $this->Auth())) { + $this->tabHooks[$hookName] = $hook; + $tabs->add($hookName, [ + 'label' => $hook->getLabel(), + 'url' => $isService ? 'monitoring/service/tabhook' : 'monitoring/host/tabhook', + 'urlParams' => $params + [ 'hook' => $hookName ] + ]); + } + } + + $tabs->extend(new DashboardAction())->extend(new MenuAction()); + } + + /** + * Create quick action forms and pass them to the view + */ + protected function setupQuickActionForms() + { + $auth = $this->Auth(); + if ($auth->hasPermission('monitoring/command/schedule-check') + || ($auth->hasPermission('monitoring/command/schedule-check/active-only') + && $this->object->active_checks_enabled + ) + ) { + $this->view->checkNowForm = $checkNowForm = new CheckNowCommandForm(); + $checkNowForm + ->setObjects($this->object) + ->handleRequest(); + } + if (! in_array((int) $this->object->state, array(0, 99)) + && $this->object->acknowledged + && $auth->hasPermission('monitoring/command/remove-acknowledgement') + ) { + $this->view->removeAckForm = $removeAckForm = new RemoveAcknowledgementCommandForm(); + $removeAckForm + ->setObjects($this->object) + ->handleRequest(); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php b/modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php new file mode 100644 index 0000000..50b6c65 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php @@ -0,0 +1,105 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Helper; + +use Icinga\Application\Logger; +use Icinga\Web\Hook; + +/** + * Renderer for plugin output based on hooks + */ +class PluginOutputHookRenderer +{ + /** @var array */ + protected $commandMap = []; + + /** + * Register PluginOutput hooks + * + * Map PluginOutput hooks to their responsible commands. + * + * @return $this + */ + public function registerHooks() + { + if (! Hook::has('monitoring/PluginOutput')) { + return $this; + } + + foreach (Hook::all('monitoring/PluginOutput') as $hook) { + /** @var \Icinga\Module\Monitoring\Hook\PluginOutputHook $hook */ + try { + $commands = $hook->getCommands(); + } catch (\Exception $e) { + Logger::error( + 'Failed to get applicable commands from hook "%s". An error occurred: %s', + get_class($hook), + $e + ); + + continue; + } + + if (! is_array($commands)) { + $commands = [$commands]; + } + + foreach ($commands as $command) { + if (! isset($this->commandMap[$command])) { + $this->commandMap[$command] = []; + } + + $this->commandMap[$command][] = $hook; + } + } + + return $this; + } + + protected function renderCommand($command, $output, $detail) + { + if (isset($this->commandMap[$command])) { + foreach ($this->commandMap[$command] as $hook) { + /** @var \Icinga\Module\Monitoring\Hook\PluginOutputHook $hook */ + + try { + $output = $hook->render($command, $output, $detail); + } catch (\Exception $e) { + Logger::error( + 'Failed to render plugin output from hook "%s". An error occurred: %s', + get_class($hook), + $e + ); + + continue; + } + } + } + + return $output; + } + + /** + * Render the given plugin output based on the specified check command + * + * Traverse all hooks which are responsible for the specified check command and call their `render()` methods. + * + * @param string $command Check command + * @param string $output Plugin output + * @param bool $detail Whether the output is requested from the detail area + * + * @return string + */ + public function render($command, $output, $detail) + { + if (empty($this->commandMap)) { + return $output; + } + + $output = $this->renderCommand('*', $output, $detail); + $output = $this->renderCommand($command, $output, $detail); + + return $output; + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php b/modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php new file mode 100644 index 0000000..fdfe18f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php @@ -0,0 +1,15 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Hook; + +use Icinga\Module\Monitoring\Hook\HostActionsHook as BaseHook; + +/** + * Compat only + * + * Please implement hooks in our Hook direcory + */ +abstract class HostActionsHook extends BaseHook +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php b/modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php new file mode 100644 index 0000000..0ffbf45 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php @@ -0,0 +1,15 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Hook; + +use Icinga\Module\Monitoring\Hook\ServiceActionsHook as BaseHook; + +/** + * Compat only + * + * Please implement hooks in our Hook direcory + */ +abstract class ServiceActionsHook extends BaseHook +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php b/modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php new file mode 100644 index 0000000..f6f110f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php @@ -0,0 +1,15 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Hook; + +use Icinga\Module\Monitoring\Hook\TimelineProviderHook as BaseHook; + +/** + * Compat only + * + * Please implement hooks in our Hook direcory + */ +abstract class TimelineProviderHook extends BaseHook +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/Action.php b/modules/monitoring/library/Monitoring/Web/Navigation/Action.php new file mode 100644 index 0000000..7e4ffe3 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Navigation/Action.php @@ -0,0 +1,123 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Navigation; + +use Icinga\Data\Filter\Filter; +use Icinga\Web\Navigation\NavigationItem; +use Icinga\Module\Monitoring\Object\Macro; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use Icinga\Web\Url; + +/** + * Action for monitored objects + */ +class Action extends NavigationItem +{ + /** + * Whether this action's macros were already resolved + * + * @var bool + */ + protected $resolved = false; + + /** + * This action's object + * + * @var MonitoredObject + */ + protected $object; + + /** + * The filter to use when being asked whether to render this action + * + * @var string + */ + protected $filter; + + /** + * This action's raw url attribute + * + * @var string + */ + protected $rawUrl; + + /** + * Set this action's object + * + * @param MonitoredObject $object + * + * @return $this + */ + public function setObject(MonitoredObject $object) + { + $this->object = $object; + return $this; + } + + /** + * Return this action's object + * + * @return MonitoredObject + */ + public function getObject() + { + return $this->object; + } + + /** + * Set the filter to use when being asked whether to render this action + * + * @param string $filter + * + * @return $this + */ + public function setFilter($filter) + { + $this->filter = $filter; + return $this; + } + + /** + * Return the filter to use when being asked whether to render this action + * + * @return string + */ + public function getFilter() + { + return $this->filter; + } + + public function setUrl($url) + { + if (is_string($url)) { + $this->rawUrl = $url; + } else { + parent::setUrl($url); + } + + return $this; + } + + public function getUrl() + { + $url = parent::getUrl(); + if (! $this->resolved && $url === null && $this->rawUrl !== null) { + $this->setUrl(Url::fromPath(Macro::resolveMacros($this->rawUrl, $this->getObject()))); + $this->resolved = true; + return parent::getUrl(); + } else { + return $url; + } + } + + public function getRender() + { + if ($this->render === null) { + $filter = $this->getFilter(); + $this->render = $filter ? Filter::fromQueryString($filter)->matches($this->getObject()) : true; + } + + return $this->render; + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php b/modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php new file mode 100644 index 0000000..2e950f1 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Navigation; + +/** + * A host action + */ +class HostAction extends Action +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php b/modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php new file mode 100644 index 0000000..2cf0cdf --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Navigation; + +/** + * A host note + */ +class HostNote extends Action +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php b/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php new file mode 100644 index 0000000..e06526e --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php @@ -0,0 +1,167 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Navigation\Renderer; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filterable; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer; + +/** + * Render generic DataView columns as badges in menu items + * + * It is possible to configure the class of the rendered badge as option 'class', the + * columns to fetch using the option 'columns' and the DataView from which the columns + * will be fetched using the option 'dataview'. + */ +class MonitoringBadgeNavigationItemRenderer extends BadgeNavigationItemRenderer +{ + /** + * Cached count + * + * @var int + */ + protected $count; + + /** + * Caches the responses for all executed summaries + * + * @var array + */ + protected static $summaries = array(); + + /** + * Accumulates all needed columns for a view to allow fetching the needed columns in + * one single query + * + * @var array + */ + protected static $dataViews = array(); + + /** + * The dataview referred to by the navigation item + * + * @var string + */ + protected $dataView; + + /** + * The columns and titles displayed in the badge + * + * @var array + */ + protected $columns; + + /** + * Set the dataview referred to by the navigation item + * + * @param string $dataView + * + * @return $this + */ + public function setDataView($dataView) + { + $this->dataView = $dataView; + return $this; + } + + /** + * Return the dataview referred to by the navigation item + * + * @return string + */ + public function getDataView() + { + return $this->dataView; + } + + /** + * Set the columns and titles displayed in the badge + * + * @param array $columns + * + * @return $this + */ + public function setColumns(array $columns) + { + $this->columns = $columns; + return $this; + } + + /** + * Return the columns and titles displayed in the badge + * + * @return array + */ + public function getColumns() + { + return $this->columns; + } + + /** + * Apply a restriction on the given data view + * + * @param string $restriction The name of restriction + * @param Filterable $filterable The filterable to restrict + * + * @return Filterable The filterable + */ + protected static function applyRestriction($restriction, Filterable $filterable) + { + $restrictions = Filter::matchAny(); + foreach (Auth::getInstance()->getRestrictions($restriction) as $filter) { + $restrictions->addFilter(Filter::fromQueryString($filter)); + } + $filterable->applyFilter($restrictions); + return $filterable; + } + + /** + * Fetch the dataview from the database + * + * @return object + */ + protected function fetchDataView() + { + $summary = MonitoringBackend::instance()->select()->from( + $this->getDataView(), + array_keys($this->getColumns()) + ); + static::applyRestriction('monitoring/filter/objects', $summary); + return $summary->fetchRow(); + } + + /** + * {@inheritdoc} + */ + public function getCount() + { + if ($this->count === null) { + try { + $summary = $this->fetchDataView(); + } catch (Exception $e) { + Logger::debug($e); + $this->count = 1; + $this->state = static::STATE_UNKNOWN; + $this->title = $e->getMessage(); + return $this->count; + } + $count = 0; + $titles = array(); + foreach ($this->getColumns() as $column => $title) { + if (isset($summary->$column) && $summary->$column > 0) { + $titles[] = sprintf($title, $summary->$column); + $count += $summary->$column; + } + } + $this->count = $count; + $this->title = implode('. ', $titles); + } + + return $this->count; + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php new file mode 100644 index 0000000..a88e94f --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Navigation; + +/** + * A service action + */ +class ServiceAction extends Action +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php new file mode 100644 index 0000000..4858bf5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Navigation; + +/** + * A service note + */ +class ServiceNote extends Action +{ +} diff --git a/modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php b/modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php new file mode 100644 index 0000000..fcbe0ca --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php @@ -0,0 +1,297 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Rest; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Util\Json; +use Icinga\Module\Monitoring\Exception\CurlException; + +/** + * REST Request + */ +class RestRequest +{ + /** + * Request URI + * + * @var string + */ + protected $uri; + + /** + * Request method + * + * @var string + */ + protected $method; + + /** + * Request content type + * + * @var string + */ + protected $contentType; + + /** + * Whether to authenticate with basic auth + * + * @var bool + */ + protected $hasBasicAuth; + + /** + * Auth username + * + * @var string + */ + protected $username; + + /** + * Auth password + * + * @var string + */ + protected $password; + + /** + * Request payload + * + * @var mixed + */ + protected $payload; + + /** + * Whether strict SSL is enabled + * + * @var bool + */ + protected $strictSsl = true; + + /** + * Request timeout + * + * @var int + */ + protected $timeout = 30; + + /** + * Create a GET REST request + * + * @param string $uri + * + * @return static + */ + public static function get($uri) + { + $request = new static; + $request->uri = $uri; + $request->method = 'GET'; + return $request; + } + + /** + * Create a POST REST request + * + * @param string $uri + * + * @return static + */ + public static function post($uri) + { + $request = new static; + $request->uri = $uri; + $request->method = 'POST'; + return $request; + } + + /** + * Send content type JSON + * + * @return $this + */ + public function sendJson() + { + $this->contentType = 'application/json'; + + return $this; + } + + /** + * Set basic auth credentials + * + * @param string $username + * @param string $password + * + * @return $this + */ + public function authenticateWith($username, $password) + { + $this->hasBasicAuth = true; + $this->username = $username; + $this->password = $password; + + return $this; + } + + /** + * Set request payload + * + * @param mixed $payload + * + * @return $this + */ + public function setPayload($payload) + { + $this->payload = $payload; + + return $this; + } + + /** + * Disable strict SSL + * + * @return $this + */ + public function noStrictSsl() + { + $this->strictSsl = false; + + return $this; + } + + /** + * Serialize payload according to content type + * + * @param mixed $payload + * @param string $contentType + * + * @return string + */ + public function serializePayload($payload, $contentType) + { + switch ($contentType) { + case 'application/json': + $payload = Json::encode($payload); + break; + } + + return $payload; + } + + /** + * Send the request + * + * @return mixed + * + * @throws Exception + */ + public function send() + { + $defaults = array( + 'host' => 'localhost', + 'path' => '/' + ); + + $url = array_merge($defaults, parse_url($this->uri)); + + if (isset($url['port'])) { + $url['host'] .= sprintf(':%u', $url['port']); + } + + if (isset($url['query'])) { + $url['path'] .= sprintf('?%s', $url['query']); + } + + $headers = array( + "{$this->method} {$url['path']} HTTP/1.1", + "Host: {$url['host']}", + "Content-Type: {$this->contentType}", + 'Accept: application/json', + // Bypass "Expect: 100-continue" timeouts + 'Expect:' + ); + + $options = array( + CURLOPT_URL => $this->uri, + CURLOPT_TIMEOUT => $this->timeout, + // Ignore proxy settings + CURLOPT_PROXY => '', + CURLOPT_CUSTOMREQUEST => $this->method + ); + + // Record cURL command line for debugging + $curlCmd = array('curl', '-s', '-X', $this->method, '-H', escapeshellarg('Accept: application/json')); + + if ($this->strictSsl) { + $options[CURLOPT_SSL_VERIFYHOST] = 2; + $options[CURLOPT_SSL_VERIFYPEER] = true; + } else { + $options[CURLOPT_SSL_VERIFYHOST] = false; + $options[CURLOPT_SSL_VERIFYPEER] = false; + $curlCmd[] = '-k'; + } + + if ($this->hasBasicAuth) { + $options[CURLOPT_USERPWD] = sprintf('%s:%s', $this->username, $this->password); + $curlCmd[] = sprintf('-u %s:%s', escapeshellarg($this->username), escapeshellarg($this->password)); + } + + if (! empty($this->payload)) { + $payload = $this->serializePayload($this->payload, $this->contentType); + $options[CURLOPT_POSTFIELDS] = $payload; + $curlCmd[] = sprintf('-d %s', escapeshellarg($payload)); + } + + $options[CURLOPT_HTTPHEADER] = $headers; + + $stream = null; + $logger = Logger::getInstance(); + if ($logger !== null && $logger->getLevel() === Logger::DEBUG) { + $stream = fopen('php://temp', 'w'); + $options[CURLOPT_VERBOSE] = true; + $options[CURLOPT_STDERR] = $stream; + } + + Logger::debug( + 'Executing %s %s', + implode(' ', $curlCmd), + escapeshellarg($this->uri) + ); + + $result = $this->curlExec($options); + + if (is_resource($stream)) { + rewind($stream); + Logger::debug(stream_get_contents($stream)); + fclose($stream); + } + + return Json::decode($result, true); + } + + /** + * Set up a new cURL handle with the given options and call {@link curl_exec()} + * + * @param array $options + * + * @return string The response + * + * @throws CurlException + */ + protected function curlExec(array $options) + { + $ch = curl_init(); + $options[CURLOPT_RETURNTRANSFER] = true; + curl_setopt_array($ch, $options); + $result = curl_exec($ch); + + if ($result === false) { + throw new CurlException('%s', curl_error($ch)); + } + + curl_close($ch); + return $result; + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php b/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php new file mode 100644 index 0000000..4cbdad5 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php @@ -0,0 +1,270 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Widget; + +use Icinga\Module\Monitoring\Hook\CustomVarRendererHook; +use Icinga\Module\Monitoring\Object\MonitoredObject; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\Widget\Icon; + +class CustomVarTable extends BaseHtmlElement +{ + /** @var iterable The variables */ + protected $data; + + /** @var ?MonitoredObject The object the variables are bound to */ + protected $object; + + /** @var Closure Callback to apply hooks */ + protected $hookApplier; + + /** @var array The groups as identified by hooks */ + protected $groups = []; + + /** @var string Header title */ + protected $headerTitle; + + /** @var int The nesting level */ + protected $level = 0; + + protected $tag = 'table'; + + /** @var HtmlElement The table body */ + protected $body; + + protected $defaultAttributes = [ + 'class' => ['custom-var-table', 'name-value-table'] + ]; + + /** + * Create a new CustomVarTable + * + * @param iterable $data + * @param ?MonitoredObject $object + */ + public function __construct($data, MonitoredObject $object = null) + { + $this->data = $data; + $this->object = $object; + $this->body = new HtmlElement('tbody'); + } + + /** + * Set the header to show + * + * @param string $title + * + * @return $this + */ + protected function setHeader($title) + { + $this->headerTitle = (string) $title; + + return $this; + } + + /** + * Add a new row to the body + * + * @param mixed $name + * @param mixed $value + * + * @return void + */ + protected function addRow($name, $value) + { + $this->body->addHtml(new HtmlElement( + 'tr', + Attributes::create(['class' => "level-{$this->level}"]), + new HtmlElement('th', null, Html::wantHtml($name)), + new HtmlElement('td', null, Html::wantHtml($value)) + )); + } + + /** + * Render a variable + * + * @param mixed $name + * @param mixed $value + * + * @return void + */ + protected function renderVar($name, $value) + { + if ($this->object !== null && $this->level === 0) { + list($name, $value, $group) = call_user_func($this->hookApplier, $name, $value); + if ($group !== null) { + $this->groups[$group][] = [$name, $value]; + return; + } + } + + $isArray = is_array($value); + if (! $isArray && $value instanceof \stdClass) { + $value = (array) $value; + $isArray = true; + } + + switch (true) { + case $isArray && is_int(key($value)): + $this->renderArray($name, $value); + break; + case $isArray: + $this->renderObject($name, $value); + break; + default: + $this->renderScalar($name, $value); + } + } + + /** + * Render an array + * + * @param mixed $name + * @param array $array + * + * @return void + */ + protected function renderArray($name, array $array) + { + $numItems = count($array); + $name = (new HtmlDocument())->addHtml( + Html::wantHtml($name), + Text::create(' (Array)') + ); + + $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems)); + + ++$this->level; + + ksort($array); + foreach ($array as $key => $value) { + $this->renderVar("[$key]", $value); + } + + --$this->level; + } + + /** + * Render an object (associative array) + * + * @param mixed $name + * @param array $object + * + * @return void + */ + protected function renderObject($name, array $object) + { + $numItems = count($object); + $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems)); + + ++$this->level; + + ksort($object); + foreach ($object as $key => $value) { + $this->renderVar($key, $value); + } + + --$this->level; + } + + /** + * Render a scalar + * + * @param mixed $name + * @param mixed $value + * + * @return void + */ + protected function renderScalar($name, $value) + { + if ($value === '') { + $value = new HtmlElement('span', Attributes::create(['class' => 'empty']), Text::create(t('empty string'))); + } + + $this->addRow($name, $value); + } + + /** + * Render a group + * + * @param string $name + * @param iterable $entries + * + * @return void + */ + protected function renderGroup($name, $entries) + { + $table = new self($entries); + + $wrapper = $this->getWrapper(); + if ($wrapper === null) { + $wrapper = new HtmlDocument(); + $wrapper->addHtml($this); + $this->prependWrapper($wrapper); + } + + $wrapper->addHtml($table->setHeader($name)); + } + + protected function assemble() + { + if ($this->object !== null) { + $this->hookApplier = CustomVarRendererHook::prepareForObject($this->object); + } + + if ($this->headerTitle !== null) { + $this->getAttributes() + ->add('class', 'collapsible') + ->add('data-visible-height', 100) + ->add('data-toggle-element', 'thead') + ->add( + 'id', + preg_replace('/\s+/', '-', strtolower($this->headerTitle)) . '-customvars' + ); + + $this->addHtml(new HtmlElement('thead', null, new HtmlElement( + 'tr', + null, + new HtmlElement( + 'th', + Attributes::create(['colspan' => 2]), + new HtmlElement( + 'span', + null, + new Icon('angle-right'), + new Icon('angle-down') + ), + Text::create($this->headerTitle) + ) + ))); + } + + if (is_array($this->data)) { + ksort($this->data); + } + + foreach ($this->data as $name => $value) { + $this->renderVar($name, $value); + } + + $this->addHtml($this->body); + + // Hooks can return objects as replacement for keys, hence a generator is needed for group entries + $genGenerator = function ($entries) { + foreach ($entries as list($key, $value)) { + yield $key => $value; + } + }; + + foreach ($this->groups as $group => $entries) { + $this->renderGroup($group, $genGenerator($entries)); + } + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php b/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php new file mode 100644 index 0000000..48b98ac --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php @@ -0,0 +1,120 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Widget; + +use Icinga\Web\Form; +use Icinga\Web\Request; +use Icinga\Web\Widget\AbstractWidget; + +class SelectBox extends AbstractWidget +{ + /** + * The name of the form that will be created + * + * @var string + */ + private $name; + + /** + * An array containing all intervals with their associated labels + * + * @var array + */ + private $values; + + /** + * The label displayed next to the select box + * + * @var string + */ + private $label; + + /** + * The name of the url parameter to set + * + * @var string + */ + private $parameter; + + /** + * A request object used for initial form population + * + * @var Request + */ + private $request; + + /** + * Create a TimelineIntervalBox + * + * @param string $name The name of the form that will be created + * @param array $values An array containing all intervals with their associated labels + * @param string $label The label displayed next to the select box + * @param string $param The request parameter name to set + */ + public function __construct($name, array $values, $label = 'Select', $param = 'selection') + { + $this->name = $name; + $this->values = $values; + $this->label = $label; + $this->parameter = $param; + } + + /** + * Apply the parameters from the given request on this widget + * + * @param Request $request The request to use for populating the form + */ + public function applyRequest(Request $request) + { + $this->request = $request; + } + + /** + * Return the chosen interval value or null + * + * @param Request $request The request to fetch the value from + * + * @return string|null + */ + public function getInterval(Request $request = null) + { + if ($request === null && $this->request) { + $request = $this->request; + } + + if ($request) { + return $request->getParam('interval'); + } + } + + /** + * Renders this widget and returns the HTML as a string + * + * @return string + */ + public function render() + { + $form = new Form(); + $form->setAttrib('class', Form::DEFAULT_CLASSES . ' inline'); + $form->setMethod('GET'); + $form->setUidDisabled(); + $form->setTokenDisabled(); + $form->setName($this->name); + $form->addElement( + 'select', + $this->parameter, + array( + 'label' => $this->label, + 'multiOptions' => $this->values, + 'autosubmit' => true + ) + ); + + if ($this->request) { + $form->populate($this->request->getParams()); + } + + return $form; + } +} diff --git a/modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php b/modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php new file mode 100644 index 0000000..fdaac51 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php @@ -0,0 +1,341 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Monitoring\Web\Widget; + +use Icinga\Web\Form; +use Icinga\Web\Navigation\Navigation; +use Icinga\Web\Navigation\NavigationItem; +use Icinga\Web\Url; +use Icinga\Web\Widget\AbstractWidget; +use Icinga\Data\Filter\Filter; + +class StateBadges extends AbstractWidget +{ + /** + * CSS class for the widget + * + * @var string + */ + const CSS_CLASS = 'state-badges'; + + /** + * State critical + * + * @var string + */ + const STATE_CRITICAL = 'state-critical'; + + /** + * State critical handled + * + * @var string + */ + const STATE_CRITICAL_HANDLED = 'state-critical handled'; + + /** + * State down + * + * @var string + */ + const STATE_DOWN = 'state-down'; + + /** + * State down handled + * + * @var string + */ + const STATE_DOWN_HANDLED = 'state-down handled'; + + /** + * State ok + * + * @var string + */ + const STATE_OK = 'state-ok'; + + /** + * State pending + * + * @var string + */ + const STATE_PENDING = 'state-pending'; + + /** + * State unknown + * + * @var string + */ + const STATE_UNKNOWN = 'state-unknown'; + + /** + * State unknown handled + * + * @var string + */ + const STATE_UNKNOWN_HANDLED = 'state-unknown handled'; + + /** + * State unreachable + * + * @var string + */ + const STATE_UNREACHABLE = 'state-unreachable'; + + /** + * State unreachable handled + * + * @var string + */ + const STATE_UNREACHABLE_HANDLED = 'state-unreachable handled'; + + /** + * State up + * + * @var string + */ + const STATE_UP = 'state-up'; + + /** + * State warning + * + * @var string + */ + const STATE_WARNING = 'state-warning'; + + /** + * State warning handled + * + * @var string + */ + const STATE_WARNING_HANDLED = 'state-warning handled'; + + /** + * State badges + * + * @var object[] + */ + protected $badges = array(); + + /** + * Internal counter for badge priorities + * + * @var int + */ + protected $priority = 1; + + /** + * The base filter applied to any badge link + * + * @var Filter + */ + protected $baseFilter; + + /** + * Base URL + * + * @var Url + */ + protected $url; + + /** + * Get the base URL + * + * @return Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the base URL + * + * @param Url|string $url + * + * @return $this + */ + public function setUrl($url) + { + if (! $url instanceof $url) { + $url = Url::fromPath($url); + } + $this->url = $url; + return $this; + } + + /** + * Get the base filter + * + * @return Filter + */ + public function getBaseFilter() + { + return $this->baseFilter; + } + + /** + * Set the base filter + * + * @param Filter $baseFilter + * + * @return $this + */ + public function setBaseFilter($baseFilter) + { + $this->baseFilter = $baseFilter; + return $this; + } + + /** + * Add a state badge + * + * @param string $state + * @param int $count + * @param array $filter + * @param string $translateSingular + * @param string $translatePlural + * @param array $translateArgs + * + * @return $this + */ + public function add( + $state, + $count, + array $filter, + $translateSingular, + $translatePlural, + array $translateArgs = array() + ) { + $this->badges[$state] = (object) array( + 'count' => (int) $count, + 'filter' => $filter, + 'translateArgs' => $translateArgs, + 'translatePlural' => $translatePlural, + 'translateSingular' => $translateSingular + ); + return $this; + } + + /** + * Create a badge + * + * @param string $state + * @param Navigation $badges + * + * @return $this + */ + public function createBadge($state, Navigation $badges) + { + if ($this->has($state)) { + $badge = $this->get($state); + $url = clone $this->url->setParams($badge->filter); + if (isset($this->baseFilter)) { + $url->addFilter($this->baseFilter); + } + $badges->addItem(new NavigationItem($state, array( + 'attributes' => array('class' => 'badge ' . $state), + 'label' => $badge->count, + 'priority' => $this->priority++, + 'title' => vsprintf( + mtp('monitoring', $badge->translateSingular, $badge->translatePlural, $badge->count), + $badge->translateArgs + ), + 'url' => $url + ))); + } + return $this; + } + + /** + * Create a badge group + * + * @param array $states + * @param Navigation $badges + * + * @return $this + */ + public function createBadgeGroup(array $states, Navigation $badges) + { + $group = array_intersect_key($this->badges, array_flip($states)); + if (! empty($group)) { + $groupItem = new NavigationItem( + uniqid(), + array( + 'cssClass' => 'state-badge-group', + 'label' => '', + 'priority' => $this->priority++ + ) + ); + $groupBadges = new Navigation(); + $groupBadges->setLayout(Navigation::LAYOUT_TABS); + foreach (array_keys($group) as $state) { + $this->createBadge($state, $groupBadges); + } + $groupItem->setChildren($groupBadges); + $badges->addItem($groupItem); + } + return $this; + } + + /** + * Get whether a badge for the given state has been added + * + * @param string $state + * + * @return bool + */ + public function has($state) + { + return isset($this->badges[$state]) && $this->badges[$state]->count; + } + + /** + * Get the badge for the given state + * + * @param string $state + * + * @return object + */ + public function get($state) + { + return $this->badges[$state]; + } + + /** + * {@inheritdoc} + */ + public function render() + { + $badges = new Navigation(); + $badges->setLayout(Navigation::LAYOUT_TABS); + $this + ->createBadgeGroup( + array(static::STATE_CRITICAL, static::STATE_CRITICAL_HANDLED), + $badges + ) + ->createBadgeGroup( + array(static::STATE_DOWN, static::STATE_DOWN_HANDLED), + $badges + ) + ->createBadgeGroup( + array(static::STATE_WARNING, static::STATE_WARNING_HANDLED), + $badges + ) + ->createBadgeGroup( + array(static::STATE_UNREACHABLE, static::STATE_UNREACHABLE_HANDLED), + $badges + ) + ->createBadgeGroup( + array(static::STATE_UNKNOWN, static::STATE_UNKNOWN_HANDLED), + $badges + ) + ->createBadge(static::STATE_OK, $badges) + ->createBadge(static::STATE_UP, $badges) + ->createBadge(static::STATE_PENDING, $badges); + return $badges + ->getRenderer() + ->setCssClass(static::CSS_CLASS) + ->render(); + } +} diff --git a/modules/monitoring/module.info b/modules/monitoring/module.info new file mode 100644 index 0000000..82c520d --- /dev/null +++ b/modules/monitoring/module.info @@ -0,0 +1,5 @@ +Module: monitoring +Version: 2.11.4 +Description: Icinga monitoring module + IDO accessor and UI for your monitoring. This is the initial instalment for a + graphical presentation of Icinga environments. The predecessor of Icinga DB. diff --git a/modules/monitoring/public/css/module.less b/modules/monitoring/public/css/module.less new file mode 100644 index 0000000..f97031c --- /dev/null +++ b/modules/monitoring/public/css/module.less @@ -0,0 +1,1919 @@ +/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +.monitoring-statusbar { + position: relative; + background-color: @body-bg-color; + border-top: 1px solid @gray-lighter; + padding: .25em @gutter; + line-height: 1.3; + + .services-summary, + .hosts-summary { + float: right; + margin-bottom: 0; + } + + .selection-info { + float: left; + margin-top: 0.182em; + } +} + +// Hostgroup- and servicegroup-grid styles + +.grid-toggle-link { + display: inline-block; + margin-left: 1em; + text-decoration: none; + vertical-align: middle; + + > i { + font-size: 1.25em; + + &.-active { + color: @icinga-blue; + } + + &.-inactive { + color: @gray-light; + } + } +} + +.group-grid { + display: grid; + grid-gap: 1em 3em; + grid-template-columns: repeat(auto-fit, 14em); + + .group-grid-cell > a:last-child { + display: inline-block; + max-width: 10em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-align: center; + vertical-align: middle; + } + + .group-grid-cell > a:first-child, + .group-grid-cell > div.state-none { + .bg-stateful(); + .rounded-corners(); + + display: inline-block; + margin-right: 1em; + padding: .5em; + height: 2.5em; + width: 2.5em; + text-align: center; + vertical-align: middle; + color: white; + } + .group-grid-cell > div.state-none { + background-color: @gray-light; + } +} + +// Styles for the icon displayed if a check result is late +.check-result-late { + &:before { + // Remove right margin because the check now form may be displayed right next to the icon and we already have a gap + // because of inline-blocks + margin-right: 0; + } +} + +// Show more and load more links in overviews +.action-links { + text-align: right; +} + +.actions .nav { + li > a, + li > span { + display: inline-block; + } +} + +// State summary badges +.state-badges { + display: inline-block; + vertical-align: middle; + + > ul > li { + padding-right: @vertical-padding; + + &:last-child { + padding-right: 0; + } + } + + .state-badge-group li { + margin-right: 1px; + } + + .state-badge-group li:last-child { + margin-right: 0; + } + + .state-badge-group .badge { + border-radius: 0; + } + + .state-badge-group li:first-child > .badge { + border-top-left-radius: 0.4em; + border-bottom-left-radius: 0.4em; + } + + .state-badge-group li:last-child > .badge { + border-top-right-radius: 0.4em; + border-bottom-right-radius: 0.4em; + } +} + +// Performance data pie charts +.inline-pie { + display: inline-block; + height: 14/12em; + margin-right: 0.1em; + position: relative; + top: 0.1em; + width: 14/12em; +} + +// Host and service summaries in detail and list views +.hosts-summary, +.services-summary { + display: inline-block; + margin-bottom: 0.5em; + + > .hosts-link, + > .services-link, + > .state-badges { + vertical-align: middle; + } +} + +.service-on { + color: @text-color-light; + + > a { + color: @text-color; + letter-spacing: normal; + font-weight: bold; + } +} + +// State table in the host and service multi-selection and detail views +.host-detail-state, +.service-detail-state { + margin-bottom: 0.5em; +} + +.grid { + .hosts-summary, + .services-summary { + float: left; + } +} + +// Quick actions +.quick-actions { + margin: 0 -.5em; + + &:last-child { + margin-bottom: -.25em; + } + + li { + color: @icinga-blue; + } + + a, + button { + .rounded-corners(); + padding: .25em .5em; + + &:hover { + background-color: @gray-lighter; + text-decoration: none; + } + } +} + +/* Generic box element */ + +.boxview > div.box { + text-align: center; + vertical-align: top; + display: inline-block; + padding: 20px; +} + + + +/* Box body of contents */ + +.boxview div.box.contents { + padding-top: 20px; +} + +.boxview div.box.contents table { + width: 100%; +} + +.boxview div.box.contents td { + vertical-align: top; +} + +/* Box entry */ + +/* Any line of a box entry */ +.boxview div.box.entry a { + display: block; +} + +.boxview div.box.badge { + padding: 5px; +} + + +/* First line of a box entry */ +.boxview div.box.entry a:first-child { +} + +/* End of generic box element */ + +/* Tactical overview element styles */ + +.tactical > .boxview > div.box { + min-height: 45em; + padding: 0px; +} + +.tactical div.box.header { + margin: 10px; + min-height: 8em; + color: @text-color-inverted; + font-size: @font-size-dashboard; +} + +.tactical div.box.badge { + border-radius: 0.0em; +} + +div.box.ok_hosts.state_up { + background-color: @color-ok; + border: 1px solid white; +} + +div.box.problem_hosts.state_down { + background-color: @color-critical; + border: 1px solid white; +} + +div.box.ok_hosts div.box.entry, div.box.problem_hosts div.box.entry { + min-width: 8em; + min-height: 4em; +} + +.tactical div.box.contents { + background-color: white; + min-height: 13em; + font-size: @font-size-dashboard-small; + text-align: left; +} + +div.box.monitoringfeatures { + border: 1px solid @gray-lighter; + border-left: 15px @gray; +} + +div.box.monitoringfeatures div.box-separator { + color: white; + background-color: @color-ok; +} + +div.box.monitoringfeatures div.feature-highlight { + background-color: @color-critical; +} + +div.box.monitoringfeatures a.feature-highlight { +} + +div.box.hostservicechecks { + border: 1px solid @gray-lighter; + border-left: 15px @gray; +} + +div.box.hostservicechecks th { + padding-bottom: 20px; +} + +/* Monitoring health - PROCESS - element styles */ + +div.box.process { + width: 100%; + max-width: 50em; + border: 1px solid @gray-lighter; + border-left: 15px @gray; + margin-bottom: 1em; + margin-right: 1em; +} + +.process div.box.header { + min-height: 5em; + border-bottom: 1px solid @gray-lighter; +} + +.process > .boxview > div.box { + min-height: 30em; +} + +.process h2 { + margin-top: 0; + margin-bottom: 1em; + padding-bottom: 1em; + border-bottom: 1px solid @gray-lighter; +} + +.process th { + width: 50%; + text-align: right; +} + +.process td { + width: 50%; + padding-left: 2em; + text-align: left; +} + +div.backend-running { + background: @color-ok; + color: white; + text-align: center; + margin-top: 1em; + padding: 0.5em; + + &.span { + color: white; + } +} + +div.backend-not-running { + background: @color-critical; + color: white; + text-align: center; + padding: 0.1em; +} + + +/* Monitoring health - FEATURE - element styles */ + +div.box.features { + width: 100%; + max-width: 50em; + border: 1px solid @gray-lighter; + border-left: 15px @gray; +} + +.features div.box.header { + min-height: 5em; + border-bottom: 1px solid @gray-lighter; +} + +.features > .boxview > div.box { + min-height: 30em; +} + +.features h2 { + margin-top: 0; + margin-bottom: 1em; + padding-bottom: 1em; + border-bottom: 1px solid @gray-lighter; +} + + +/* Monitoring health - STATS - element styles */ + +div.box.stats { + width: 100%; + max-width: 50em; + border: 1px solid @gray-lighter; + border-left: 15px @gray; + color: @text-color; +} + +.stats > .boxview > div.box { + min-height: 30em; +} + +.stats > .name-value-table { + table-layout: fixed; + text-align: left; +} + +.stats > table > thead { + color: @gray; +} + +.stats > h2 { + text-align: left; + border-bottom: 1px solid @gray-lighter; + + > .hosts-summary, + > .services-summary { + width: 100%; + > .state-badges { + float: right; + } + } +} + +.tinystatesummary .badge { + font-weight: normal; +} + +/* Monitoring timeline styles */ + +div.timeline-legend { + padding: 0.5em; + margin-top: 2em; + border: 1px solid @gray-lighter; + border-left-width: 15px; + + h2 { + margin: 0; + margin-left: 0.5em; + line-height: 1.1em; + } + + & > span { + display: inline-block; + padding: 0.5em; + margin: 0.5em; + + span { + white-space: nowrap; + min-width: 25px; + font-family: tahoma, verdana, sans-serif; + font-weight: @font-weight-bold; + font-size: 11px; + text-align: center; + color: @text-color-inverted; + padding-left: 5px; + padding-right: 5px; + padding-top: 2px; + padding-bottom: 2px; + } + } +} + +div.timeline { + div.timeframe { + height: 7em; + margin-bottom: 1em; + clear: left; + + span { + width: 8em; + margin-top: 2.3em; + margin-right: 1.5em; + display: block; + float: left; + text-align: center; + + a { + font-weight: bold; + white-space: nowrap; + } + } + + div.circle-box { + // width: inline-style; + height: 100%; + margin-right: 0.5em; + position: relative; + float: left; + + div.outer-circle { + // width: inline-style; + // height: inline-style; + position: absolute; + top: 50%; + // margin-top: inline-style; + + &.extrapolated { + border-width: 2px; + border-style: dotted; + //border-color: inline-style; + border-radius: 100%; + // background-color: inline-style; + } + + a.inner-circle { + // width: inline-style; + // height: inline-style; + display: block; + position: absolute; + top: 50%; + left: 50%; + // margin-top: inline-style; + // margin-left: inline-style; + border-radius: 100%; + // background-color: inline-style; + } + } + } + } + + hr { + border: 0; + height: 1px; + background-image: linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); + background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); + background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); + background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); + background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); + } +} + +@timeline-notification-color: #3a71ea; +@timeline-hard-state-color: #ff7000; +@timeline-comment-color: #79bdba; +@timeline-ack-color: #a2721d; +@timeline-downtime-start-color: #8e8e8e; +@timeline-downtime-end-color: #d5d6ad; + +.timeline-notification { + background-color: @timeline-notification-color; + + &.extrapolated { + background-color: lighten(@timeline-notification-color, 20%); + } +} + +.timeline-hard-state { + background-color: @timeline-hard-state-color; + + &.extrapolated { + background-color: lighten(@timeline-hard-state-color, 20%); + } +} + +.timeline-comment { + background-color: @timeline-comment-color; + + &.extrapolated { + background-color: lighten(@timeline-comment-color, 20%); + } +} + +.timeline-ack { + background-color: @timeline-ack-color; + + &.extrapolated { + background-color: lighten(@timeline-ack-color, 20%); + } +} + +.timeline-downtime-start { + background-color: @timeline-downtime-start-color; + + &.extrapolated { + background-color: lighten(@timeline-downtime-start-color, 20%); + } +} + +.timeline-downtime-end { + background-color: @timeline-downtime-end-color; + + &.extrapolated { + background-color: lighten(@timeline-downtime-end-color, 20%); + } +} + +/* End of monitoring timeline styles */ + +/* Object features */ + +form.instance-features span.description, form.object-features span.description { + text-align: left; +} + +.object-features { + .control-label-group { + text-align: left; + margin-right: 0; + width: @name-value-table-name-width; + color: @text-color-light; + + label { + font-size: inherit; + } + } + + .control-group { + margin-top: 0; + margin-bottom: 0; + + &.indeterminate { + justify-content: flex-start; + + .control-label-group { + flex: 0 1 auto; + } + + select { + width: auto; + flex: 0 1 auto; + + & + span.hint { + flex: 0 1 auto; + } + } + } + } + + .toggle-switch { + margin-left: @table-column-padding; + } + + select { + margin-right: .5em; + margin-left: @table-column-padding; + + & + span.hint { + margin: .35em; + color: @gray-light; + font-style: italic; + } + } +} + +.plugin-output { + border-left: 5px solid @gray-lighter; + padding: 0.66em 0.33em; + + .state-critical { + background-color: @color-critical; + color: @body-bg-color; + padding: 0.2em; + } + + .state-ok { + background-color: @color-ok; + color: @body-bg-color; + padding: 0.2em; + } + + .state-unknown { + background-color: @color-unknown; + color: @body-bg-color; + padding: 0.2em; + } + + .state-warning { + background-color: @color-warning; + color: @body-bg-color; + padding: 0.2em; + } + + .state-down { + background-color: @color-down; + color: @body-bg-color; + padding: 0.2em; + } + + .state-up { + background-color: @color-up; + color: @body-bg-color; + padding: 0.2em; + } +} + +.go-ahead, +.markdown, +.plugin-output { + a { + border-bottom: 1px dotted @gray-light; + + &:hover { + border-bottom: 1px solid @text-color; + text-decoration: none; + } + } +} + +.event-details { + .badge { + font-size: 0.6em; + margin-right: 0.5em; + } + + .state-label { + vertical-align: middle; + } +} + +/* Object customvars */ +.custom-var-table { + .level-1 th { + padding-left: .5em; + } + + .level-2 th { + padding-left: 1em; + } + + .level-3 th { + padding-left: 1.5em; + } + + .level-4 th { + padding-left: 2em; + } + + .level-5 th { + padding-left: 2.5em; + } + + .level-6 th { + padding-left: 3em; + } + + .empty { + color: @gray-semilight; + } + + thead th { + padding-left: 0; + text-align: left; + font-weight: bold; + font-size: 1.167em; + + > span { + :nth-child(1), + :nth-child(2) { + display: none; + } + } + } + + &[data-can-collapse] thead th > span { + :nth-child(1) { + display: none; + } + + :nth-child(2) { + display: inline-block; + } + } + + &.collapsed thead th > span { + :nth-child(1) { + display: inline-block; + } + + :nth-child(2) { + display: none; + } + } +} + +//p.pluginoutput { +// width: 100%; +// white-space: pre-wrap; +// font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'DejaVu Sans Mono', 'Courier New', Courier, monospace; +//} +// +//table.action td .pluginoutput { +// font-size: 0.875em; +// line-height: 1.2em; +// padding: 0; +// margin: 0; +//} +// +//div.pluginoutput { +// overflow: auto; +// color: #888; +// margin-bottom: 1em; +// padding: 0.2em; +//} +// +//div.pluginoutput pre { +// white-space: pre-wrap; +// border-left: 4px solid #d8d8d8; +// padding: 0.3em 0 0.3em 1em; +// font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'DejaVu Sans Mono', 'Courier New', Courier, monospace; +//} +// +//table.objectstate td.state { +// padding-top: 0.5em; +// padding-bottom: 0.5em; +//} +// +//div.contacts div.contact { +// background-color: #eee; +// padding: 0.5em; +// border: 1px solid #d9d9d9; +// overflow: hidden; +// margin: 0.125em; +// float: left; +//} +// +//div.contacts div.contact a{ +// color: @colorTextDefault; +//} +// +//div.contacts div.contact > img { +// width: 80px; +// height: 80px; +// margin-right: 8px; +// float: left; +//} +// +//div.contacts div.notification-periods { +// margin-top: 0.5em; +//} +// +//.tinystatesummary { +// .badges { +// display: inline-block; +// margin-bottom: 4px; +// margin-left: 1em; +// height: auto; +// } +// +// .state > a { +// color: white; +// font-size: 0.857em; +// padding: 2px 5px; +// } +//} +// +///* State badges */ +//span.state { +// font-weight: bold; +// color: white; +// font-weight: bold; +// padding: 2px 3px; +// margin-right: 5px; +//} +// +//span.state.active { +// border: 2px solid #555; +// padding: 2px 4px; +// margin-right: 4px; +//} +// +//span.state span.state { +// margin: 0 -6px 0 5px; +//} +// +//span.state.ok { +// background: @colorOk; +//} +// +//span.state.up { +// background: @colorOk; +//} +// +//span.state.critical { +// background: @colorCritical; +//} +// +//span.state.down { +// background: @colorCritical; +//} +// +//span.state.handled.critical { +// background: @colorCriticalHandled; +//} +// +//span.state.handled.down { +// background: @colorCriticalHandled; +//} +// +//span.state.warning { +// background: @colorWarning; +//} +// +//span.state.handled.warning { +// background: @colorWarningHandled; +//} +// +//span.state.unknown { +// background: @colorUnknown; +//} +// +//span.state.handled.unknown { +// background: @colorUnknownHandled; +//} +// +//span.state.pending { +// background: @colorPending; +//} +// +//form.instance-features span.description, form.object-features span.description { +// display: inline; +//} +// +//.boxview div.box form.instance-features div.header { +// border-bottom: 1px solid #d9d9d9; +// margin-bottom: 0.5em; +// +// h2 { +// border: 0; +// padding-bottom: 0; +// } +//} +// +//table.avp form.object-features div.header h4 { +// margin: 0; +//} +// +//table.avp { +// th { +// font-weight: normal; +// font-size: 0.875em; +// padding-top: 0.25em; +// } +// +// h2 { +// font-size: 0.875em; +// line-height: 1.2em; +// padding-bottom: 0.1em; +// } +// +// td { +// color: #666; +// padding-bottom: 0.3em; +// line-height: 1.5em; +// th, td { +// padding: 0; +// } +// } +// +// .badge a[href] { +// color: @colorGray; +// } +// +// .go-ahead { +// a, button.link-like { +// color: #222; +// } +// } +// +// .object-features { +// label { +// font-weight: normal; +// margin-right: 0; +// width: 14em; +// font-size: 0.875em; +// } +// +// input { +// margin: 0; +// } +// } +//} +// +//table.avp .customvar ul { +// list-style-type: none; +// margin: 0; +// padding: 0; +// padding-left: 1.5em; +//} +// +//div.selection-info { +// padding-top: 0.4em; +// float: right; +// cursor: help; +// font-size: 0.857em; +//} +// +//.optionbox { +// margin-left: 0em; +// margin-right: 3em; +//} +// +//.optionbox label { +// max-width: 6.5em; +// text-align: left; +// vertgical-align: middle; +// margin-right: 0em; +//} +// +//.optionbox input { +// vertical-align: middle; +//} +// +//.object-command form h1, .objects-command form h1 { +// border: none; +//} +// +//hr.command-separator { +// border: none; +// border-bottom: 2px solid @colorPetrol; +//} +// +//div.backend-not-running { +// background: @colorCritical; +// color: white; +// text-align: center; +// padding: 0.1em; +//} +// +//td.state { +// .time-ago, +// .time-since, +// .time-until { +// text-transform: capitalize; +// } +//} +// +//.inline-comments { +// padding: 0; +// margin: 0; +// font-size: 0.857em; +// +// .time-ago { +// font-style: italic; +// color: #919191; +// } +// +// li { +// list-style-type: none; +// margin-bottom: 8px; +// } +// +// h3 { +// border: none; +// border-bottom: 1px solid gray; +// font-weight: normal; +// font-size: inherit; +// color: inherit; +// margin: 0; +// padding-bottom: 0.1em; +// } +// +// h3 .author { +// font-weight: bold; +// } +// +// h3 form { +// display: none; +// } +// +// h3 form { +// float: right; +// } +// +// li:hover h3 { +// background: #F9F9F9; +// position: relative; +// +// form { +// display: inline; +// } +// } +// +// p { +// margin: 0; +// +// a { +// color: #222; +// } +// } +//} +// +///* Special tables and states */ +// +//table.colors { +// font-size: 0.8em; +// width: 98%; +// margin: 0 1%; +//} +// +//table.colors td { +// text-align: center; +// vertical-align: middle; +// width: 10%; +// height: 1.6em; +// font-weight: normal; +// border: 0.079em solid white; +//} +// +//table.action td.state, table.objectstate td.state { +// font-size: 0.857em; +// text-align: center; +//} +// +// +///* State row behaviour */ +// +//tr.state img.icon { +// margin-right: 2px; +//} +// +///* Hostgroup badge quickfix */ +//tr.state span a { +// color: white; +// font-size: 0.857em; +// padding: 2px 5px; +//} +// +//tr.state:hover a { +// color: inherit; +//} +// +//tr.state a.active { +//} +// +//tr.state.new td.state { +// font-weight: bold; +//} +// +//tr.state td.state { +// width: 9em; +// color: white; +// border-bottom: none; +//} +// +//tr.state.handled td.state, tr.state.ok td.state, tr.state.up td.state, tr.state.pending td.state { +// border-left-style: solid; +// border-left-width: 1.5em; +// padding-left: 0em; +// padding-right: 0.5em; +// color: black; +// background-color: transparent; +//} +// +//tr.state.ok td.state, tr.state.up td.state { +// border-left-color: @colorOk; +//} +// +//tr.state.warning td.state { +// background-color: @colorWarning; +//} +// +//tr.state.warning.handled td.state { +// border-left-color: @colorWarningHandled; +//} +// +//tr.state.critical td.state, tr.state.down td.state { +// background-color: @colorCritical; +//} +// +//tr.state.critical.handled td.state, tr.state.down.handled td.state { +// border-left-color: @colorCriticalHandled; +//} +// +//tr.state.unreachable td.state { +// background-color: @colorUnreachable; +//} +// +//tr.state.unreachable.handled td.state { +// border-left-color: @colorUnreachableHandled; +//} +// +//tr.state.unknown td.state { +// background-color: @colorUnknown; +//} +// +//tr.state.unknown.handled td.state { +// border-left-color: @colorUnknownHandled; +//} +// +//tr.state.pending td.state { +// border-left-color: @colorPending; +//} +// +//tr.state.invalid td.state { +// background-color: @colorInvalid; +//} +// +//tr.state.unreachable td.state { +// background-color: @colorUnreachable; +//} +// +//tr.state.unreachable.handled td.state { +// border-left-color: @colorUnreachableHandled; +//} +// +//tr.state.handled td.state { +// color: inherit; +// background-color: transparent !important; +//} +// +///* HOVER colors */ +// +//tr.state[href]:hover td.state { +// background-color: #eee; +//} +// +//tr.state.ok[href]:hover, tr.state.up[href]:hover { +// background-color: @colorOk; +//} +// +//tr.state.handled[href]:hover, tr.state.handled[href]:hover td.state { +// color: #121212 !important; +//} +// +//tr.state.warning[href]:hover { +// background-color: @colorWarning; +// color: white; +//} +// +//tr.state.warning.handled[href]:hover { +// background-color: @colorWarningHandled; +//} +// +//tr.state.critical[href]:hover, tr.state.down[href]:hover { +// background-color: @colorCritical; +// color: white; +//} +// +//tr.state.critical.handled[href]:hover, tr.state.down.handled[href]:hover { +// background-color: @colorCriticalHandled; +// color: #333; +//} +// +//tr.state.unknown[href]:hover { +// background-color: @colorUnknown; +// color: white; +//} +// +//tr.state.unknown.handled[href]:hover { +// background-color: @colorUnknownHandled; +//} +// +//tr.state.pending[href]:hover { +// background-color: @colorPending; +//} +// +//tr.state.invalid[href]:hover { +// background-color: @colorInvalid; +// color: white; +//} +// +//tr.state.unreachable[href]:hover { +// background-color: @colorUnreachable; +//} +// +//tr.state.unreachable.handled[href]:hover { +// background-color: @colorUnreachableHandled; +//} +// +//tr.state[href]:hover td.state { +// background-color: inherit !important; +//} +// +///* END of HOVER colors */ +// +///* END of special tables and states */ +// +// +///* Generic colors */ +// +//a.critical { +// color: @colorCritical; +//} +// +///* END of Generic colors */ +// +// +///* Generic box element */ +// +//.boxview a { +// text-decoration: none; +//} +// +//.boxview > div.box { +// text-align: center; +// vertical-align: top; +// display: inline-block; +// padding: 0.4em; +// margin: 0.4em; +// border: 1px solid #d9d9d9; +// background: #eee; +//} +// +///* Box header */ +//.boxview div.box.header { +// padding-bottom: 0.5em; +// margin-bottom: 0.5em; +// border-bottom: 1px solid #888; +//} +// +//.boxview div.box.header h2 { +// margin-top: 0.1em; +// margin-bottom: 0; +// font-size: 0.8em; +// border-bottom: none; +// color: @colorTextDefault; +//} +// +//.boxview div.box.header h2:first-child { +// margin-top: 0.2em; +// font-size: inherit; +// color: @colorTextDefault; +//} +// +//.boxview div.box.header h2 > a { +// color: inherit; +//} +// +//.boxview div.box.header h2 > a:hover { +// text-decoration: underline; +//} +// +//.boxview div.box.header h3 { +// line-height: 1.5em; +// font-size: 0.9em; +// color: #555; +//} +// +///* Box body of contents */ +//.boxview div.box.contents { +// padding: 0.2em; +//} +// +//.boxview div.box.contents table { +// width: 100%; +//} +// +//.boxview div.box.contents td { +// width: 13em; +// vertical-align: top; +//} +// +///* Box separator */ +//.boxview div.box-separator:first-child { +// border-top-width: 0; +//} +// +//.boxview div.box-separator { +// font-size: 0.8em; +// padding: 0.4em 0 0.4em; +// border: 1px solid #d9d9d9; +// +// font-weight: bold; +// letter-spacing: 0.1em; +//} +// +///* Box entry */ +//.boxview div.box.entry { +// min-height: 2.7em; +// margin: 0.2em; +// font-size: 0.9em; +// white-space: nowrap; +// +// color: @colorTextDefault; +//} +// +///* Any line of a box entry */ +//.boxview div.box.entry a { +// display: block; +// +// color: inherit; +//} +// +//.boxview div.box.entry a:hover { +// color: @colorTextDefault; +//} +// +///* First line of a box entry */ +//.boxview div.box.entry a:first-child { +// font-size: 1em; +//} +// +///* End of generic box element */ +// +// +///* Monitoring box element styles */ +// +///* Host- and Servicegroup element styles */ +// +//div.box.entry.state_up, div.box.entry.state_ok { +// border: 1px solid @colorOk; +// border-left: 1em solid @colorOk; +//} +// +//div.box.entry.state_pending { +// border: 1px solid @colorPending; +// border-left: 1em solid @colorPending; +//} +// +//div.box.entry.state_down, div.box.entry.state_critical { +// border: 1px solid @colorCritical; +// border-left: 1em solid @colorCritical; +// background-color: @colorCritical; +// color: white; +//} +// +//div.box.entry.state_down a:hover, div.box.entry.state_critical a:hover { +// color: #dcdcdc; +//} +// +//div.box.entry.state_warning { +// border: 1px solid @colorWarning; +// border-left: 1em solid @colorWarning; +// background-color: @colorWarning; +// color: white; +//} +// +//div.box.entry.state_warning a:hover { +// color: #dcdcdc; +//} +// +//div.box.entry.state_unreachable, div.box.entry.state_unknown { +// border: 1px solid @colorUnknown; +// border-left: 1em solid @colorUnknown; +// background-color: @colorUnknown; +// color: white; +//} +// +//div.box.entry.state_unreachable a:hover, div.box.entry.state_unknown a:hover { +// color: #dcdcdc; +//} +// +//div.box.entry.handled { +// background-color: transparent; +// color: inherit; +//} +// +//div.box.entry.handled a:hover { +// color: @colorTextDefault; +//} +// +/* Tactical overview element styles */ +// +//.tactical > .boxview > div.box { +// min-height: 20em; +// min-width: 12.1em; +//} +// +//.tactical div.box.contents { +// min-height: 14.5em; +//} +// +//div.box.contents.zero { +// min-width: 11.1em; +// +// background-color: transparent; +//} +// +//div.box.contents.zero span { +// font-weight: bold; +// line-height: 2em; +// +// color: #666; +//} +// +//div.box.contents.zero h3 { +// margin: 0; +// font-size: 12em; +// line-height: 1em; +// +// color: #666; +//} +// +//div.box.ok_hosts.state_up { +// border: 5px solid @colorOk; +//} +// +//div.box.ok_hosts.state_pending { +// background-color: @colorPending; +//} +// +//div.box.problem_hosts.state_down { +// border: 5px solid @colorCritical; +//} +// +//div.box.problem_hosts.state_down.handled { +// background-color: @colorCriticalHandled; +//} +// +//div.box.problem_hosts.state_unreachable { +// background-color: @colorUnreachable; +//} +// +//div.box.problem_hosts.state_unreachable.handled { +// background-color: @colorUnreachableHandled; +//} +// +//div.box.ok_hosts div.box.entry, div.box.problem_hosts div.box.entry { +// min-width: 11.1em; +//} +// +//div.box.monitoringfeatures div.box.contents { +// padding: 0 2 0em; +//} +// +//div.box.monitoringfeatures { +// border: 5px solid #d9d9d9; +//} +// +//div.box.monitoringfeatures div.box-separator { +// color: white; +// background-color: @colorOk; +//} +// +//div.box.monitoringfeatures div.feature-highlight { +// background-color: @colorCritical; +//} +// +//div.box.monitoringfeatures a.feature-highlight { +// font-weight: bold; +//} +// +//div.box.hostservicechecks { +// border: 5px solid #d9d9d9; +//} +// +///* Contactgroup element styles */ +// +//div.box.contactgroup { +// width: 18em; +// padding: 0.8em; +//} +// +//div.box.contactgroup div.box.contents { +// padding: 0.6em; +//} +// +//div.box.contactgroup div.box.entry { +// overflow: hidden; +// clear: left; +//} +// +//div.box.contactgroup div.box.entry img { +// width: 80px; +// height: 80px; +// float: left; +// +//} +// +//div.box.contactgroup div.box.entry a { +// margin-top: 0.4em; +// +// font-weight: bold; +//} +// +//div.box.contactgroup div.box.entry p { +// margin: 0.4em 0 0; +//} +// +//div.circular { +// margin-top: 0.5em; +// margin-left: 2em; +// margin-right: 1em; +// width: 80px; +// height: 80px; +// float: left; +// background-size: 100% 100%; +//} +// +///* End of monitoring box element styles */ +// +// +///* Monitoring pivot table styles */ +// +//div.pivot-pagination { +// margin: 1em; +// +// table { +// table-layout: fixed; +// border-spacing: 1px; +// border-collapse: separate; +// border: 1px solid LightGrey; +// border-radius: 0.3em; +// +// td { +// width: 16px; +// height: 16px; +// padding: 0; +// background-color: #fbfbfb; +// +// &:hover, &.active { +// background-color: #e5e5e5; +// } +// +// a { +// width: 16px; +// height: 16px; +// display: block; +// } +// } +// } +//} +// +//table.joystick-pagination { +// margin-top: -1.5em; +// +// td { +// width: 1.25em; +// height: 1.3em; +// } +//} +// +///* End of monitoring pivot table styles */ +// +///* Monitoring timeline styles */ +// +//div.timeline-legend { +// float: left; +// padding: 0.5em; +// border: 1px solid #d9d9d9; +// background-color: #eee; +// +// h2 { +// margin: 0; +// margin-left: 0.5em; +// line-height: 1.1em; +// } +// +// & > span { +// display: inline-block; +// padding: 0.5em; +// margin: 0.5em; +// +// span { +// color: white; +// font-size: 0.8em; +// font-weight: bold; +// white-space: nowrap; +// } +// } +//} +// +//div.timeline { +// div.timeframe { +// height: 7em; +// margin-bottom: 1em; +// clear: left; +// +// span { +// width: 8em; +// margin-top: 2.3em; +// margin-right: 1.5em; +// display: block; +// float: left; +// text-align: center; +// +// a { +// color: @colorTextDefault; +// font-size: 0.8em; +// font-weight: bold; +// text-decoration: none; +// white-space: nowrap; +// +// &:hover { +// color: @colorTextDefault; +// text-decoration: underline; +// +// } +// } +// } +// +// div.circle-box { +// // width: inline-style; +// height: 100%; +// margin-right: 0.5em; +// position: relative; +// float: left; +// +// div.outer-circle { +// // width: inline-style; +// // height: inline-style; +// position: absolute; +// top: 50%; +// // margin-top: inline-style; +// +// &.extrapolated { +// border-width: 2px; +// border-style: dotted; +// //border-color: inline-style; +// border-radius: 100%; +// // background-color: inline-style; +// } +// +// a.inner-circle { +// // width: inline-style; +// // height: inline-style; +// display: block; +// position: absolute; +// top: 50%; +// left: 50%; +// // margin-top: inline-style; +// // margin-left: inline-style; +// border-radius: 100%; +// // background-color: inline-style; +// } +// } +// } +// } +// +// hr { +// border: 0; +// height: 1px; +// background-image: linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); +// background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); +// background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); +// background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); +// background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0)); +// } +//} +// +///* End of monitoring timeline styles */ +// +///* Monitoring groupsummary styles */ +// +//.dashboard table.groupview { +// margin-top: 0; +//} +// +//table.groupview { +// width: 100%; +// margin-top: 1em; +// border-collapse: separate; +// border-spacing: 0.1em; +// +// th { +// font-size: 1.0em; +// font-weight: normal; +// text-align: center; +// white-space: nowrap; +// border-bottom: 2px solid @gray-light; +// } +// +// td { +// &.groupname { +// width: 60%; +// +// a { +// color: inherit; +// text-decoration: none; +// +// &:hover { +// text-decoration: underline; +// } +// } +// } +// +// &.total { +// width: 10%; +// } +// +// &.state { +// width: 20%; +// white-space: nowrap; +// +// &.change { +// width: 10%; +// text-align: center; +// border-left-width: 1.5em; +// border-left-style: solid; +// padding: 0.3em 0.5em 0.3em 0.5em; +// +// strong { +// font-size: 0.8em; +// } +// +// &.ok { +// border-color: @colorOk; +// } +// +// &.pending { +// border-color: @colorPending; +// } +// +// &.warning { +// border-color: @colorWarningHandled; +// +// &.unhandled { +// color: white; +// border-left-width: 0; +// background-color: @colorWarning; +// } +// } +// +// &.unknown { +// border-color: @colorUnknownHandled; +// +// &.unhandled { +// color: white; +// border-left-width: 0; +// background-color: @colorUnknown; +// } +// } +// +// &.critical { +// border-color: @colorCriticalHandled; +// +// &.unhandled { +// color: white; +// border-left-width: 0; +// background-color: @colorCritical; +// } +// } +// } +// +// span.state { +// &.handled { +// margin-right: 2px; +// } +// +// a { +// font-size: 0.9em; +// color: white; +// text-decoration: none; +// +// &:hover { +// text-decoration: underline; +// } +// } +// } +// } +// } +//} +// +///* End of monitoring groupsummary styles */ +// +///* compact table */ +//table.statesummary { +// text-align: left; +// width: auto; +// border-collapse: separate; +// +// tr.state td.state { +// width: auto; +// font-weight: bold; +// } +// +// td { +// font-size: 0.9em; +// line-height: 1.2em; +// padding-left: 0.2em; +// margin: 0; +// } +// +// td.state { +// padding: 0.2em; +// min-width: 75px; +// font-size: 0.75em; +// text-align: center; +// } +// +// td.name { +// font-weight: bold; +// } +// +// td a { +// color: inherit; +// text-decoration: none; +// } +//} +// +//table.action .objectflags { +// float: right; +//} +// +//table.objectstate { +// border-collapse: separate; +// border-spacing: 1px; +//} +// +//table.objectstate td { +// padding-left: 1em; +//} +// +//table.objectstate tr.state td.state { +// width: 9em; +// text-align: center; +// padding-left: 0; +// border-radius: 0; +//} +// +//table.avp td.performance-data { +// padding: 0.3em 0 0.3em 1em; +//} +// +//table.perfdata { +// min-width: 24em; +// font-size: 0.9em; +// width: 100%; +//} +// +//table.perfdata th { +// padding: 0; +// text-align: left; +// padding-right: 0.5em; +//} +// +//table.perfdata td { +// white-space: nowrap; +// padding-right: 0.5em; +//} diff --git a/modules/monitoring/public/css/service-grid.less b/modules/monitoring/public/css/service-grid.less new file mode 100644 index 0000000..fd22097 --- /dev/null +++ b/modules/monitoring/public/css/service-grid.less @@ -0,0 +1,75 @@ +/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +.service-grid-table { + width: 0; + white-space: nowrap; + + td { + color: @gray-light; + padding: 0.2em; + text-align: center; + width: 1em; + } + + .rotate-45 { + height: 8em; + + div { + .transform(translate(0.4em, 2.8em) rotate(315deg)); + width: 1.5em; + } + } + + .service-grid-table-more { + text-align: center; + a { + display: inline; + } + } +} + +.joystick-pagination { + margin: 0 auto; + font-size: 130%; + + a { + color: @text-color; + outline: none; + + &:hover { + color: @text-color-light; + } + &:focus, &:active { + color: @icinga-blue; + } + } + + i { + display: block; + height: 1.5em; + width: 1.5em; + } +} + +.service-grid-link { + .bg-stateful(); + .rounded-corners(); + + display: inline-block; + height: 1.5em; + vertical-align: middle; + width: 1.5em; +} + +form.filter-toggle { + label:not(.toggle-switch) { + display: inline-block; + vertical-align: top; + margin-left: .5em; + color: @gray-light; + } + + input[type="checkbox"]:checked ~ label { + color: inherit; + } +} diff --git a/modules/monitoring/public/css/tables.less b/modules/monitoring/public/css/tables.less new file mode 100644 index 0000000..c5b5f27 --- /dev/null +++ b/modules/monitoring/public/css/tables.less @@ -0,0 +1,282 @@ +/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +@border-left-width: 6px; + +// Icon images in list and detail views +.host-icon-image, +.service-icon-image { + max-width: 2em; + vertical-align: middle; +} + +// Check source reachable information in the host and service detail views +.check-source-meta { + font-size: @font-size-small; +} + +// Object link and comment author in the comment overview +.comment-author { + margin-bottom: 0.25em; + + > a { + font-weight: bold; + } +} + +// Comment icons, e.g. persistent in the comment overview +.comment-icons { + float: right; +} + +.caption { + height: 3em; + text-overflow: ellipsis; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + + img { + max-height: 1em; + } +} + +// Type information for backends in the monitoring config +.config-label-meta { + font-size: @font-size-small; +} + +// Column for counts, e.g. host group members +.count-col { + width: 4em; +} + +// Custom variables in the host and service detail view +.custom-variables > ul { + list-style-type: none; + margin: 0; +} + +// Host name and IP addresses in the host and service detail view +.host-meta { + color: @text-color-light; + font-size: @font-size-small; +} + +// Notification recipient in the notifications overview +.notification-recipient { + color: @text-color-light; + float: right; + font-size: @font-size-small; +} + + +// Container for plugin output and performance data in overviews +.overview-plugin-output-container { + .clearfix(); +} + +// Performance data pies in overviews +.overview-performance-data { + float: right; + font-size: @font-size-small; +} + +// Plugin output in detail views +.plugin-output, +// Plugin output in overviews +.overview-plugin-output { + -webkit-hyphens: auto; + -moz-hyphens: auto; + -ms-hyphens: auto; + hyphens: auto; + + overflow-wrap: break-word; + word-wrap: break-word; +} + +// Plugin output in overviews +.overview-plugin-output { + color: @text-color-light; + font-family: @font-family-fixed; + font-size: @font-size-small; + margin: 0; + white-space: pre-wrap; + // Long text in table cells overflows the table's width if the table's layout is not fixed. + // Thus overflow-wrap will not have any effect. But w/ the following we set a width of any value + // plus a min-width of 100% to consume the full width nonetheless which seems to always + // instruct browsers to not overflow the table. Ridiculous. + min-width: 100%; + width: 1em; +} + +// Table for performance data in detail views +.performance-data-table { + display: block; + overflow-x: auto; + position: relative; + + > thead > tr > th { + text-align: left; + } + + > thead > tr > th:first-child, + > tbody > tr > td:first-child { + // Reset base padding + padding-left: 0; + } + + > thead > tr > th, + > tbody > tr > td { + white-space: nowrap; + } +} + +// Performance data table column for sparkline pie charts in detail views +.sparkline-col { + width: 2em; +} + +// Service description if in the service detail view +.service-meta { + color: @text-color-light; + font-size: @font-size-small; +} + +// State column for label and duration in overviews +.state-col { + &.state-ok, + &.state-up { + border-left: @border-left-width solid @color-ok; + } + + &.state-pending { + border-left: @border-left-width solid @color-pending; + } + + &.state-critical, + &.state-down { + background-color: @color-critical; + color: @text-color-inverted; + + &.handled { + background-color: inherit; + color: inherit; + border-left: @border-left-width solid @color-critical-handled; + } + } + + &.state-warning { + background-color: @color-warning; + color: @text-color-inverted; + + &.handled { + background-color: inherit; + color: inherit; + border-left: @border-left-width solid @color-warning-handled; + } + } + + &.state-unknown { + background-color: @color-unknown; + color: @text-color-inverted; + + &.handled { + background-color: inherit; + color: inherit; + border-left: @border-left-width solid @color-unknown-handled; + } + } + + &.state-unreachable { + background-color: @color-unreachable; + color: @text-color-inverted; + + &.handled { + background-color: inherit; + color: inherit; + border-left: @border-left-width solid @color-unreachable-handled; + } + } + + // State class for history events + &.state-no-state { + border-left: @border-left-width solid @text-color-light; + } + + * { + color: inherit; + } + + text-align: center; + width: 8em; +} + +// Wraps links, icons and meta in overviews +.state-header { + .clearfix(); + + > a { + font-weight: bold; + } +} + +// State icons, e.g. acknowledged in overviews +.state-icons { + float: right; +} + +// State labels in overviews +.state-label { + font-family: @font-family-wide; + font-size: @font-size-small; + letter-spacing: 1px; +} + +// State duration and state type information in overviews +.state-meta { + font-size: @font-size-small; +} + +.state-table { + border-collapse: separate; + border-spacing: 0 1px; + width: 100%; + + tr[href] { + -webkit-transform: translate3d(0,0,0); /* Without this, hovering in Safari is broken in history table rows */ + -moz-transform: none; /* Firefox collapses border spacing due to the above */ + } + + tr[href].active { + background-color: @tr-active-color; + } + + tr[href]:hover { + background-color: @tr-hover-color; + cursor: pointer; + } + + tr[href].state-outdated:not(:hover):not(.active) td:not(.state-col) { + opacity: 0.7; + } +} + +// Event history +.history-message-container { + display: flex; + align-items: center; + justify-content: center; + + > .history-message-icon { + padding: 0.25em; + } + + > .history-message-output { + flex: 1; + + > a { + font-weight: bold; + } + } +} diff --git a/modules/monitoring/public/js/module.js b/modules/monitoring/public/js/module.js new file mode 100644 index 0000000..d665e6b --- /dev/null +++ b/modules/monitoring/public/js/module.js @@ -0,0 +1,84 @@ +/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +(function(Icinga) { + + var Monitoring = function(module) { + /** + * The Icinga.Module instance + */ + this.module = module; + + /** + * The observer used to handle the timeline's infinite loading + */ + this.scrollCheckTimer = null; + + /** + * Whether to skip the timeline's scroll-check + */ + this.skipScrollCheck = false; + + this.initialize(); + }; + + Monitoring.prototype = { + + initialize: function() + { + this.module.on('rendered', this.enableScrollCheck); + this.module.icinga.logger.debug('Monitoring module loaded'); + }, + + /** + * Enable the timeline's scroll-check + */ + enableScrollCheck: function() + { + /** + * Re-enable the scroll-check in case the timeline has just been extended + */ + if (this.skipScrollCheck) { + this.skipScrollCheck = false; + } + + /** + * Prepare the timer to handle the timeline's infinite loading + */ + var $timeline = $('div.timeline'); + if ($timeline.length && !$timeline.closest('.dashboard').length) { + if (this.scrollCheckTimer === null) { + this.scrollCheckTimer = this.module.icinga.timer.register( + this.checkTimelinePosition, + this, + 800 + ); + this.module.icinga.logger.debug('Enabled timeline scroll-check'); + } + } + }, + + /** + * Check whether the user scrolled to the end of the timeline + */ + checkTimelinePosition: function() + { + if (!$('div.timeline').length) { + this.module.icinga.timer.unregister(this.scrollCheckTimer); + this.scrollCheckTimer = null; + this.module.icinga.logger.debug('Disabled timeline scroll-check'); + } else if (!this.skipScrollCheck && this.module.icinga.utils.isVisible('#end')) { + this.skipScrollCheck = true; + this.module.icinga.loader.loadUrl( + $('#end').remove().attr('href'), + $('div.timeline'), + undefined, + undefined, + 'append' + ).addToHistory = false; + } + } + }; + + Icinga.availableModules.monitoring = Monitoring; + +}(Icinga)); diff --git a/modules/monitoring/run.php b/modules/monitoring/run.php new file mode 100644 index 0000000..6fe4921 --- /dev/null +++ b/modules/monitoring/run.php @@ -0,0 +1,8 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +/** @var $this \Icinga\Application\Modules\Module */ + +$this->provideHook('ApplicationState'); +$this->provideHook('Health'); +$this->provideHook('X509/Sni'); diff --git a/modules/setup/application/clicommands/ConfigCommand.php b/modules/setup/application/clicommands/ConfigCommand.php new file mode 100644 index 0000000..130d797 --- /dev/null +++ b/modules/setup/application/clicommands/ConfigCommand.php @@ -0,0 +1,185 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Clicommands; + +use Icinga\Application\Logger; +use Icinga\Cli\Command; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Setup\Webserver; + +class ConfigCommand extends Command +{ + /** + * Create Icinga Web 2's configuration directory + * + * USAGE: + * + * icingacli setup config directory [options] + * + * OPTIONS: + * + * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2] + * + * --mode=<mode> The access mode to use [2770] + * + * --group=<group> Owner group for the configuration directory [icingaweb2] + * + * EXAMPLES: + * + * icingacli setup config directory + * + * icingacli setup config directory --mode=2775 --config=/opt/icingaweb2/etc + */ + public function directoryAction() + { + $configDir = trim($this->params->get('config', $this->app->getConfigDir())); + if (strlen($configDir) === 0) { + $this->fail($this->translate( + 'The argument --config expects a path to Icinga Web 2\'s configuration files' + )); + } + + $group = trim($this->params->get('group', 'icingaweb2')); + if (strlen($group) === 0) { + $this->fail($this->translate( + 'The argument --group expects a owner group for the configuration directory' + )); + } + + $mode = trim($this->params->get('mode', '2770')); + if (strlen($mode) === 0) { + $this->fail($this->translate( + 'The argument --mode expects an access mode for the configuration directory' + )); + } + + if (! file_exists($configDir) && ! @mkdir($configDir, 0755, true)) { + $e = error_get_last(); + $this->fail(sprintf( + $this->translate('Can\'t create configuration directory %s: %s'), + $configDir, + $e['message'] + )); + } + + if (! @chmod($configDir, octdec($mode))) { + $e = error_get_last(); + $this->fail(sprintf( + $this->translate('Can\'t change the mode of the configuration directory to %s: %s'), + $mode, + $e['message'] + )); + } + + if (! @chgrp($configDir, $group)) { + $e = error_get_last(); + $this->fail(sprintf( + $this->translate('Can\'t change the group of %s to %s: %s'), + $configDir, + $group, + $e['message'] + )); + } + + printf($this->translate('Successfully created configuration directory %s') . PHP_EOL, $configDir); + } + + /** + * Create webserver configuration + * + * USAGE: + * + * icingacli setup config webserver <apache|nginx> [options] + * + * OPTIONS: + * + * --path=<urlpath> The URL path to Icinga Web 2 [/icingaweb2] + * + * --root|--document-root=<directory> The directory from which the webserver will serve files + * [/path/to/icingaweb2/public] + * + * --enable-fpm Enable FPM handler for Apache (Nginx is always enabled) + * + * --fpm-uri=<uri> Address or path where to pass requests to FPM [127.0.0.1:9000] + * + * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2] + * + * --file=<filename> Write configuration to file [stdout] + * + * EXAMPLES: + * + * icingacli setup config webserver apache + * + * icingacli setup config webserver apache \ + * --path=/icingaweb2 \ + * --document-root=/usr/share/icingaweb2/public \ + * --config=/etc/icingaweb2 + * + * icingacli setup config webserver apache \ + * --file=/etc/apache2/conf.d/icingaweb2.conf + * + * icingacli setup config webserver nginx \ + * --root=/usr/share/icingaweb2/public \ + * --fpm-uri=unix:/var/run/php5-fpm.sock + */ + public function webserverAction() + { + if (($type = $this->params->getStandalone()) === null) { + $this->fail($this->translate('Argument type is mandatory.')); + } + try { + $webserver = Webserver::createInstance($type); + } catch (ProgrammingError $e) { + $this->fail($this->translate('Unknown type') . ': ' . $type); + } + $urlPath = trim($this->params->get('path', $webserver->getUrlPath())); + if (strlen($urlPath) === 0) { + $this->fail($this->translate('The argument --path expects a URL path')); + } + $documentRoot = trim( + $this->params->get('root', $this->params->get('document-root', $webserver->getDocumentRoot())) + ); + if (strlen($documentRoot) === 0) { + $this->fail($this->translate( + 'The argument --root/--document-root expects a directory from which the webserver will serve files' + )); + } + $configDir = trim($this->params->get('config', $webserver->getConfigDir())); + if (strlen($configDir) === 0) { + $this->fail($this->translate( + 'The argument --config expects a path to Icinga Web 2\'s configuration files' + )); + } + + $enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm()); + + $fpmUri = trim($this->params->get('fpm-uri', $webserver->getFpmUri())); + if (empty($fpmUri)) { + $this->fail($this->translate( + 'The argument --fpm-uri expects an address or path where to pass requests to FPM' + )); + } + $webserver + ->setDocumentRoot($documentRoot) + ->setConfigDir($configDir) + ->setUrlPath($urlPath) + ->setEnableFpm($enableFpm) + ->setFpmUri($fpmUri); + $config = $webserver->generate() . "\n"; + if (($file = $this->params->get('file')) !== null) { + if (file_exists($file) === true) { + $this->fail(sprintf($this->translate('File %s already exists. Please delete it first.'), $file)); + } + Logger::info($this->translate('Write %s configuration to file: %s'), $type, $file); + $re = file_put_contents($file, $config); + if ($re === false) { + $this->fail($this->translate('Could not write to file') . ': ' . $file); + } + Logger::info($this->translate('Successfully written %d bytes to file'), $re); + return true; + } + echo $config; + return true; + } +} diff --git a/modules/setup/application/clicommands/TokenCommand.php b/modules/setup/application/clicommands/TokenCommand.php new file mode 100644 index 0000000..f1c30d1 --- /dev/null +++ b/modules/setup/application/clicommands/TokenCommand.php @@ -0,0 +1,89 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Clicommands; + +use Icinga\Cli\Command; + +/** + * Maintain the setup wizard's authentication + * + * The token command allows you to display the current setup token or to create a new one. + * + * Usage: icingacli setup token <action> + */ +class TokenCommand extends Command +{ + /** + * Display the current setup token + * + * Shows you the current setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard. + * + * USAGE: + * + * icingacli setup token show [options] + * + * OPTIONS: + * + * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2] + */ + public function showAction() + { + $configDir = $this->params->get('config', $this->app->getConfigDir()); + if (! is_string($configDir) || strlen(trim($configDir)) === 0) { + $this->fail($this->translate( + 'The argument --config expects a path to Icinga Web 2\'s configuration files' + )); + } + + $token = file_get_contents($configDir . '/setup.token'); + if (! $token) { + $this->fail( + $this->translate('Nothing to show. Please create a new setup token using the generateToken action.') + ); + } + + printf($this->translate("The current setup token is: %s\n"), $token); + } + + /** + * Create a new setup token + * + * Re-generates the setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard. + * + * USAGE: + * + * icingacli setup token create [options] + * + * OPTIONS: + * + * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2] + */ + public function createAction() + { + $configDir = $this->params->get('config', $this->app->getConfigDir()); + if (! is_string($configDir) || strlen(trim($configDir)) === 0) { + $this->fail($this->translate( + 'The argument --config expects a path to Icinga Web 2\'s configuration files' + )); + } + + $file = $configDir . '/setup.token'; + + if (function_exists('openssl_random_pseudo_bytes')) { + $token = bin2hex(openssl_random_pseudo_bytes(8)); + } else { + $token = substr(md5(mt_rand()), 16); + } + + if (false === file_put_contents($file, $token)) { + $this->fail(sprintf($this->translate('Cannot write setup token "%s" to disk.'), $file)); + } + + if (! chmod($file, 0660)) { + $this->fail(sprintf($this->translate('Cannot change access mode of "%s" to %o.'), $file, 0660)); + } + + printf($this->translate("The newly generated setup token is: %s\n"), $token); + } +} diff --git a/modules/setup/application/controllers/IndexController.php b/modules/setup/application/controllers/IndexController.php new file mode 100644 index 0000000..b75643c --- /dev/null +++ b/modules/setup/application/controllers/IndexController.php @@ -0,0 +1,91 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Controllers; + +use Icinga\Module\Setup\WebWizard; +use Icinga\Web\Controller; +use Icinga\Web\Form; +use Icinga\Web\Url; + +class IndexController extends Controller +{ + /** + * Whether the controller requires the user to be authenticated + * + * FALSE as the wizard uses token authentication + * + * @var bool + */ + protected $requiresAuthentication = false; + + /** + * {@inheritdoc} + */ + protected $innerLayout = 'inline'; + + /** + * Show the web wizard and run the configuration once finished + */ + public function indexAction() + { + $wizard = new WebWizard(); + + if ($wizard->isFinished()) { + $setup = $wizard->getSetup(); + $success = $setup->run(); + if ($success) { + $wizard->clearSession(); + } else { + $wizard->setIsFinished(false); + } + + $this->view->success = $success; + $this->view->report = $setup->getReport(); + } else { + $wizard->handleRequest(); + + $restartForm = new Form(); + $restartForm->setUidDisabled(); + $restartForm->setName('setup_restart_form'); + $restartForm->setAction(Url::fromPath('setup/index/restart')); + $restartForm->setAttrib('class', 'restart-form'); + $restartForm->addElement( + 'button', + 'btn_submit', + array( + 'type' => 'submit', + 'value' => 'btn_submit', + 'escape' => false, + 'label' => $this->view->icon('reply-all'), + 'title' => $this->translate('Restart the setup'), + 'decorators' => array('ViewHelper') + ) + ); + + $this->view->restartForm = $restartForm; + } + + $this->view->wizard = $wizard; + $this->view->title = $this->translate('Setup') . ' :: ' . $this->view->defaultTitle; + } + + /** + * Reset session and restart the wizard + */ + public function restartAction() + { + $this->assertHttpMethod('POST'); + + $form = new Form(array( + 'onSuccess' => function () { + $wizard = new WebWizard(); + $wizard->clearSession(false); + } + )); + $form->setUidDisabled(); + $form->setRedirectUrl('setup'); + $form->setSubmitLabel('btn_submit'); + $form->handleRequest(); + } +} diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php new file mode 100644 index 0000000..3252ec1 --- /dev/null +++ b/modules/setup/application/forms/AdminAccountPage.php @@ -0,0 +1,423 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Exception; +use Icinga\Application\Config; +use Icinga\Authentication\User\ExternalBackend; +use Icinga\Authentication\User\UserBackend; +use Icinga\Authentication\User\DbUserBackend; +use Icinga\Authentication\User\LdapUserBackend; +use Icinga\Authentication\UserGroup\UserGroupBackend; +use Icinga\Authentication\UserGroup\LdapUserGroupBackend; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Data\Selectable; +use Icinga\Exception\NotImplementedError; +use Icinga\Web\Form; + +/** + * Wizard page to define the initial administrative account + */ +class AdminAccountPage extends Form +{ + /** + * The resource configuration to use + * + * @var array + */ + protected $resourceConfig; + + /** + * The user backend configuration to use + * + * @var array + */ + protected $backendConfig; + + /** + * The user group backend configuration to use + * + * @var array + */ + protected $groupConfig; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_admin_account'); + $this->setTitle($this->translate('Administration', 'setup.page.title')); + $this->addDescription($this->translate( + 'Now it\'s time to configure your first administrative account or group for Icinga Web 2.' + )); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return $this + */ + public function setResourceConfig(array $config) + { + $this->resourceConfig = $config; + return $this; + } + + /** + * Set the user backend configuration to use + * + * @param array $config + * + * @return $this + */ + public function setBackendConfig(array $config) + { + $this->backendConfig = $config; + return $this; + } + + /** + * Set the user group backend configuration to use + * + * @param array $config + * + * @return $this + */ + public function setGroupConfig(array $config = null) + { + $this->groupConfig = $config; + return $this; + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $choices = array(); + if ($this->backendConfig['backend'] !== 'db') { + $choices['by_name'] = $this->translate('By Name', 'setup.admin'); + $choice = isset($formData['user_type']) ? $formData['user_type'] : 'by_name'; + + if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) { + $groups = $this->fetchGroups(); + if (! empty($groups)) { + $choices['user_group'] = $this->translate('User Group', 'setup.admin'); + } + } + } else { + $choices['new_user'] = $this->translate('New User', 'setup.admin'); + $choice = isset($formData['user_type']) ? $formData['user_type'] : 'new_user'; + } + + if (in_array($this->backendConfig['backend'], array('db', 'ldap', 'msldap'))) { + $users = $this->fetchUsers(); + if (! empty($users)) { + $choices['existing_user'] = $this->translate('Existing User', 'setup.admin'); + } + } + + if (count($choices) > 1) { + $this->addElement( + 'select', + 'user_type', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Type Of Definition'), + 'description' => $this->translate('Choose how to define the desired account.'), + 'multiOptions' => $choices, + 'value' => $choice + ) + ); + } else { + $this->addElement( + 'hidden', + 'user_type', + array( + 'required' => true, + 'value' => key($choices) + ) + ); + } + + if ($choice === 'by_name') { + $this->addElement( + 'text', + 'by_name', + array( + 'required' => true, + 'value' => $this->getUsername(), + 'label' => $this->translate('Username'), + 'description' => $this->translate( + 'Define the initial administrative account by providing a username that reflects' + . ' a user created later or one that is authenticated using external mechanisms.' + ) + ) + ); + } + + if ($choice === 'user_group') { + $this->addElement( + 'select', + 'user_group', + array( + 'required' => true, + 'label' => $this->translate('Group Name'), + 'description' => $this->translate( + 'Choose a user group reported by the LDAP backend' + . ' to permit its members administrative access.', + 'setup.admin' + ), + 'multiOptions' => array_combine($groups, $groups) + ) + ); + } + + if ($choice === 'existing_user') { + $this->addElement( + 'select', + 'existing_user', + array( + 'required' => true, + 'label' => $this->translate('Username'), + 'description' => sprintf( + $this->translate( + 'Choose a user reported by the %s backend as the initial administrative account.', + 'setup.admin' + ), + $this->backendConfig['backend'] === 'db' + ? $this->translate('database', 'setup.admin.authbackend') + : 'LDAP' + ), + 'multiOptions' => array_combine($users, $users) + ) + ); + } + + if ($choice === 'new_user') { + $this->addElement( + 'text', + 'new_user', + array( + 'required' => true, + 'label' => $this->translate('Username'), + 'description' => $this->translate( + 'Enter the username to be used when creating an initial administrative account.' + ) + ) + ); + $this->addElement( + 'password', + 'new_user_password', + array( + 'required' => true, + 'renderPassword' => true, + 'label' => $this->translate('Password'), + 'description' => $this->translate( + 'Enter the password to assign to the newly created account.' + ) + ) + ); + $this->addElement( + 'password', + 'new_user_2ndpass', + array( + 'required' => true, + 'renderPassword' => true, + 'label' => $this->translate('Repeat password'), + 'description' => $this->translate( + 'Please repeat the password given above to avoid typing errors.' + ), + 'validators' => array( + array('identical', false, array('new_user_password')) + ) + ) + ); + } + } + + /** + * Validate the given request data and ensure that any new user does not already exist + * + * @param array $data The request data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if ($data['user_type'] === 'new_user' && $this->hasUser($data['new_user'])) { + $this->getElement('new_user')->addError($this->translate('Username already exists.')); + return false; + } + + return true; + } + + /** + * Return the name of the externally authenticated user + * + * @return string + */ + protected function getUsername() + { + list($name, $_) = ExternalBackend::getRemoteUserInformation(); + if ($name === null) { + return ''; + } + + if (isset($this->backendConfig['strip_username_regexp']) && $this->backendConfig['strip_username_regexp']) { + // No need to silence or log anything here because the pattern has + // already been successfully compiled during backend configuration + $name = preg_replace($this->backendConfig['strip_username_regexp'], '', $name); + } + + return $name; + } + + /** + * Return the names of all users the user backend currently provides + * + * @return array + */ + protected function fetchUsers() + { + try { + $query = $this + ->createUserBackend() + ->select(array('user_name')) + ->order('user_name', 'asc', true); + if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) { + $query->getQuery()->setUsePagedResults(); + } + + return $query->fetchColumn(); + } catch (Exception $_) { + // No need to handle anything special here. Error means no users found. + return array(); + } + } + + /** + * Return whether the user backend provides a user with the given name + * + * @param string $username + * + * @return bool + */ + protected function hasUser($username) + { + try { + return $this + ->createUserBackend() + ->select() + ->where('user_name', $username) + ->count() > 1; + } catch (Exception $_) { + return false; + } + } + + /** + * Create and return the user backend + * + * @return DbUserBackend|LdapUserBackend + */ + protected function createUserBackend() + { + $resourceConfig = new Config(); + $resourceConfig->setSection($this->resourceConfig['name'], $this->resourceConfig); + ResourceFactory::setConfig($resourceConfig); + + $config = new ConfigObject($this->backendConfig); + $config->resource = $this->resourceConfig['name']; + return UserBackend::create(null, $config); + } + + /** + * Return the names of all user groups the user group backend currently provides + * + * @return array + */ + protected function fetchGroups() + { + try { + $query = $this + ->createUserGroupBackend() + ->select(array('group_name')); + if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) { + $query->getQuery()->setUsePagedResults(); + } + + return $query->fetchColumn(); + } catch (Exception $_) { + // No need to handle anything special here. Error means no groups found. + return array(); + } + } + + /** + * Return whether the user group backend provides a user group with the given name + * + * @param string $groupname + * + * @return bool + */ + protected function hasGroup($groupname) + { + try { + return $this + ->createUserGroupBackend() + ->select() + ->where('group_name', $groupname) + ->count() > 1; + } catch (Exception $_) { + return false; + } + } + + /** + * Create and return the user group backend + * + * @return LdapUserGroupBackend + */ + protected function createUserGroupBackend() + { + $resourceConfig = new Config(); + $resourceConfig->setSection($this->resourceConfig['name'], $this->resourceConfig); + ResourceFactory::setConfig($resourceConfig); + + $backendConfig = new Config(); + $backendConfig->setSection($this->backendConfig['name'], array_merge( + $this->backendConfig, + array('resource' => $this->resourceConfig['name']) + )); + UserBackend::setConfig($backendConfig); + + if (empty($this->groupConfig)) { + $groupConfig = new ConfigObject(array( + 'backend' => $this->backendConfig['backend'], // _Should_ be "db" or "msldap" + 'resource' => $this->resourceConfig['name'], + 'user_backend' => $this->backendConfig['name'] // Gets ignored if 'backend' is "db" + )); + } else { + $groupConfig = new ConfigObject($this->groupConfig); + } + + $backend = UserGroupBackend::create(null, $groupConfig); + if (! $backend instanceof Selectable) { + throw new NotImplementedError('Unsupported, until #9772 has been resolved'); + } + + return $backend; + } +} diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php new file mode 100644 index 0000000..4280c64 --- /dev/null +++ b/modules/setup/application/forms/AuthBackendPage.php @@ -0,0 +1,273 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Application\Config; +use Icinga\Data\ResourceFactory; +use Icinga\Forms\Config\UserBackendConfigForm; +use Icinga\Forms\Config\UserBackend\DbBackendForm; +use Icinga\Forms\Config\UserBackend\LdapBackendForm; +use Icinga\Forms\Config\UserBackend\ExternalBackendForm; +use Icinga\Web\Form; + +/** + * Wizard page to define authentication backend specific details + */ +class AuthBackendPage extends Form +{ + /** + * The resource configuration to use + * + * @var array + */ + protected $config; + + /** + * Default values for the subform's elements suggested by a previous step + * + * @var string[] + */ + protected $suggestions = array(); + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_authentication_backend'); + $this->setTitle($this->translate('Authentication Backend', 'setup.page.title')); + $this->setValidatePartial(true); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return $this + */ + public function setResourceConfig(array $config) + { + $resourceConfig = new Config(); + $resourceConfig->setSection($config['name'], $config); + ResourceFactory::setConfig($resourceConfig); + + $this->config = $config; + return $this; + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } + + if (! isset($this->config) || $this->config['type'] === 'external') { + $backendForm = new ExternalBackendForm(); + $backendForm->create($formData); + $this->addDescription($this->translate( + 'You\'ve chosen to authenticate using a web server\'s mechanism so it may be necessary' + . ' to adjust usernames before any permissions, restrictions, etc. are being applied.' + )); + } elseif ($this->config['type'] === 'db') { + $this->setRequiredCue(null); + $backendForm = new DbBackendForm(); + $backendForm->setRequiredCue(null); + $backendForm->create($formData)->removeElement('resource'); + $this->addDescription($this->translate( + 'As you\'ve chosen to use a database for authentication all you need ' + . 'to do now is defining a name for your first authentication backend.' + )); + } elseif ($this->config['type'] === 'ldap') { + $type = null; + if (! isset($formData['type'])) { + if (isset($formData['backend'])) { + $formData['type'] = $type = $formData['backend']; + } elseif (isset($this->suggestions['backend'])) { + $formData['type'] = $type = $this->suggestions['backend']; + } + } + + $backendForm = new LdapBackendForm(); + $backendForm->setSuggestions($this->suggestions); + $backendForm->setResources(array($this->config['name'])); + $backendForm->create($formData); + $backendForm->getElement('resource')->setIgnore(true); + $this->addDescription($this->translate( + 'Before you are able to authenticate using the LDAP connection defined earlier you need to' + . ' provide some more information so that Icinga Web 2 is able to locate account details.' + )); + $this->addElement( + 'select', + 'type', + array( + 'ignore' => true, + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Backend Type'), + 'description' => $this->translate( + 'The type of the resource being used for this authenticaton provider' + ), + 'multiOptions' => array( + 'ldap' => 'LDAP', + 'msldap' => 'ActiveDirectory' + ), + 'value' => $type + ) + ); + } + + $backendForm->getElement('name')->setValue('icingaweb2'); + $this->addSubForm($backendForm, 'backend_form'); + } + + /** + * Retrieve all form element values + * + * @param bool $suppressArrayNotation Ignored + * + * @return array + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues(); + $values = array_merge($values, $values['backend_form']); + unset($values['backend_form']); + return $values; + } + + /** + * Validate the given form data and check whether it's possible to authenticate using the configured backend + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (! parent::isValid($data)) { + return false; + } + + if (isset($this->config)) { + if ($this->config['type'] === 'ldap' && ( + ! isset($data['skip_validation']) || $data['skip_validation'] == 0) + ) { + $self = clone $this; + $self->getSubForm('backend_form')->getElement('resource')->setIgnore(false); + $inspection = UserBackendConfigForm::inspectUserBackend($self); + if ($inspection && $inspection->hasError()) { + $this->error($inspection->getError()); + $this->addSkipValidationCheckbox(); + return false; + } + } + } + + return true; + } + + /** + * Run the configured backend's inspection checks and show the result, if necessary + * + * This will only run any validation if the user pushed the 'backend_validation' button. + * + * @param array $formData + * + * @return bool + */ + public function isValidPartial(array $formData) + { + if (isset($formData['backend_validation']) && parent::isValid($formData)) { + $self = clone $this; + if (($resourceElement = $self->getSubForm('backend_form')->getElement('resource')) !== null) { + $resourceElement->setIgnore(false); + } + + $inspection = UserBackendConfigForm::inspectUserBackend($self); + if ($inspection !== null) { + $join = function ($e) use (&$join) { + return is_string($e) ? $e : join("\n", array_map($join, $e)); + }; + $this->addElement( + 'note', + 'inspection_output', + array( + 'order' => 0, + 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n" + . join("\n", array_map($join, $inspection->toArray())), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')), + ) + ) + ); + + if ($inspection->hasError()) { + $this->warning(sprintf( + $this->translate('Failed to successfully validate the configuration: %s'), + $inspection->getError() + )); + return false; + } + } + + $this->info($this->translate('The configuration has been successfully validated.')); + } elseif (isset($formData['discovery_btn']) || isset($formData['btn_discover_domain'])) { + return parent::isValidPartial($formData); + } elseif (! isset($formData['backend_validation'])) { + // This is usually done by isValid(Partial), but as we're not calling any of these... + $this->populate($formData); + } + + return true; + } + + /** + * Add a checkbox to this form by which the user can skip the authentication validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'order' => 0, + 'ignore' => true, + 'required' => true, + 'label' => $this->translate('Skip Validation'), + 'description' => $this->translate('Check this to not to validate authentication using this backend') + ) + ); + } + + /** + * Get default values for the subform's elements suggested by a previous step + * + * @return string[] + */ + public function getSuggestions() + { + return $this->suggestions; + } + + /** + * Set default values for the subform's elements suggested by a previous step + * + * @param string[] $suggestions + * + * @return $this + */ + public function setSuggestions(array $suggestions) + { + $this->suggestions = $suggestions; + + return $this; + } +} diff --git a/modules/setup/application/forms/AuthenticationPage.php b/modules/setup/application/forms/AuthenticationPage.php new file mode 100644 index 0000000..52e3c66 --- /dev/null +++ b/modules/setup/application/forms/AuthenticationPage.php @@ -0,0 +1,69 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Authentication\User\ExternalBackend; +use Icinga\Web\Form; +use Icinga\Application\Platform; + +/** + * Wizard page to choose an authentication backend + */ +class AuthenticationPage extends Form +{ + /** + * Initialize this page + */ + public function init() + { + $this->setRequiredCue(null); + $this->setName('setup_authentication_type'); + $this->setTitle($this->translate('Authentication', 'setup.page.title')); + $this->addDescription($this->translate( + 'Please choose how you want to authenticate when accessing Icinga Web 2.' + . ' Configuring backend specific details follows in a later step.' + )); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + if (isset($formData['type']) && $formData['type'] === 'external') { + list($username, $_) = ExternalBackend::getRemoteUserInformation(); + if ($username === null) { + $this->info( + $this->translate( + 'You\'re currently not authenticated using any of the web server\'s authentication ' + . 'mechanisms. Make sure you\'ll configure such, otherwise you\'ll not be able to ' + . 'log into Icinga Web 2.' + ), + false + ); + } + } + + $backendTypes = array(); + if (Platform::hasMysqlSupport() || Platform::hasPostgresqlSupport()) { + $backendTypes['db'] = $this->translate('Database'); + } + if (Platform::extensionLoaded('ldap')) { + $backendTypes['ldap'] = 'LDAP'; + } + $backendTypes['external'] = $this->translate('External'); + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('Authentication Type'), + 'description' => $this->translate('The type of authentication to use when accessing Icinga Web 2'), + 'multiOptions' => $backendTypes + ) + ); + } +} diff --git a/modules/setup/application/forms/DatabaseCreationPage.php b/modules/setup/application/forms/DatabaseCreationPage.php new file mode 100644 index 0000000..8660a21 --- /dev/null +++ b/modules/setup/application/forms/DatabaseCreationPage.php @@ -0,0 +1,208 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use PDOException; +use Icinga\Web\Form; +use Icinga\Module\Setup\Utils\DbTool; + +/** + * Wizard page to define a database user that is able to create databases and tables + */ +class DatabaseCreationPage extends Form +{ + /** + * The resource configuration to use + * + * @var array + */ + protected $config; + + /** + * The required privileges to setup the database + * + * @var array + */ + protected $databaseSetupPrivileges; + + /** + * The required privileges to operate the database + * + * @var array + */ + protected $databaseUsagePrivileges; + + /** + * Initialize this page + */ + public function init() + { + $this->setTitle($this->translate('Database Setup', 'setup.page.title')); + $this->addDescription($this->translate( + 'It seems that either the database you defined earlier does not yet exist and cannot be created' + . ' using the provided access credentials, the database does not have the required schema to be' + . ' operated by Icinga Web 2 or the provided access credentials do not have the sufficient ' + . 'permissions to access the database. Please provide appropriate access credentials to solve this.' + )); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return $this + */ + public function setResourceConfig(array $config) + { + $this->config = $config; + return $this; + } + + /** + * Set the required privileges to setup the database + * + * @param array $privileges The privileges + * + * @return $this + */ + public function setDatabaseSetupPrivileges(array $privileges) + { + $this->databaseSetupPrivileges = $privileges; + return $this; + } + + /** + * Set the required privileges to operate the database + * + * @param array $privileges The privileges + * + * @return $this + */ + public function setDatabaseUsagePrivileges(array $privileges) + { + $this->databaseUsagePrivileges = $privileges; + return $this; + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $skipValidation = isset($formData['skip_validation']) && $formData['skip_validation']; + $this->addElement( + 'text', + 'username', + array( + 'required' => false === $skipValidation, + 'label' => $this->translate('Username'), + 'description' => $this->translate( + 'A user which is able to create databases and/or touch the database schema' + ) + ) + ); + $this->addElement( + 'password', + 'password', + array( + 'renderPassword' => true, + 'label' => $this->translate('Password'), + 'description' => $this->translate('The password for the database user defined above') + ) + ); + + if ($skipValidation) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + } + + /** + * Validate the given form data and check whether the defined user has sufficient access rights + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (isset($data['skip_validation']) && $data['skip_validation']) { + return true; + } + + $config = $this->config; + $config['username'] = $this->getValue('username'); + $config['password'] = $this->getValue('password'); + $db = new DbTool($config); + + try { + $db->connectToDb(); // Are we able to login on the database? + } catch (PDOException $_) { + try { + $db->connectToHost(); // Are we able to login on the server? + } catch (PDOException $e) { + // We are NOT able to login on the server.. + $this->error($e->getMessage()); + $this->addSkipValidationCheckbox(); + return false; + } + } + + // In case we are connected the credentials filled into this + // form need to be granted to create databases, users... + if (false === $db->checkPrivileges($this->databaseSetupPrivileges)) { + $this->error( + $this->translate('The provided credentials cannot be used to create the database and/or the user.') + ); + $this->addSkipValidationCheckbox(); + return false; + } + + // ...and to grant all required usage privileges to others + if (false === $db->isGrantable($this->databaseUsagePrivileges)) { + $this->error(sprintf( + $this->translate( + 'The provided credentials cannot be used to grant all required privileges to the login "%s".' + ), + $this->config['username'] + )); + $this->addSkipValidationCheckbox(); + return false; + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the login and privilege validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'order' => 0, + 'required' => true, + 'label' => $this->translate('Skip Validation'), + 'description' => $this->translate( + 'Check this to not to validate the ability to login and required privileges' + ) + ) + ); + } +} diff --git a/modules/setup/application/forms/DbResourcePage.php b/modules/setup/application/forms/DbResourcePage.php new file mode 100644 index 0000000..b3f1784 --- /dev/null +++ b/modules/setup/application/forms/DbResourcePage.php @@ -0,0 +1,183 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Exception; +use Icinga\Web\Form; +use Icinga\Forms\Config\Resource\DbResourceForm; +use Icinga\Module\Setup\Utils\DbTool; + +/** + * Wizard page to define connection details for a database resource + */ +class DbResourcePage extends Form +{ + /** + * Initialize this page + */ + public function init() + { + $this->setTitle($this->translate('Database Resource', 'setup.page.title')); + $this->setValidatePartial(true); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'db' + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $resourceForm = new DbResourceForm(); + $this->addElements($resourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icingaweb_db'); + } + + /** + * Validate the given form data and check whether it's possible to connect to the database server + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if (! $this->validateConfiguration()) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Check whether it's possible to connect to the database server + * + * This will only run the check if the user pushed the 'backend_validation' button. + * + * @param array $formData + * + * @return bool + */ + public function isValidPartial(array $formData) + { + if (isset($formData['backend_validation']) && parent::isValid($formData)) { + if (! $this->validateConfiguration()) { + return false; + } + + $this->info($this->translate('The configuration has been successfully validated.')); + } elseif (! isset($formData['backend_validation'])) { + // This is usually done by isValid(Partial), but as we're not calling any of these... + $this->populate($formData); + } + + return true; + } + + /** + * Return whether the configuration is valid + * + * @return bool + */ + protected function validateConfiguration() + { + try { + $db = new DbTool($this->getValues()); + $db->checkConnectivity(); + } catch (Exception $e) { + $this->error(sprintf( + $this->translate('Failed to successfully validate the configuration: %s'), + $e->getMessage() + )); + return false; + } + + $state = true; + $connectionError = null; + + try { + $db->connectToDb(); + } catch (Exception $e) { + $connectionError = $e; + } + + if ($connectionError === null && array_search('icinga_instances', $db->listTables(), true) !== false) { + $this->warning($this->translate( + 'The database you\'ve configured to use for Icinga Web 2 seems to be the one of Icinga. Please be aware' + . ' that this database configuration is supposed to be used for Icinga Web 2\'s configuration and that' + . ' it is highly recommended to not mix different schemas in the same database. If this is intentional,' + . ' you can skip the validation and ignore this warning. If not, please provide a different database.' + )); + $state = false; + } + + if ($this->getValue('db') === 'pgsql') { + if ($connectionError !== null) { + $this->warning(sprintf( + $this->translate('Unable to check the server\'s version. This is usually not a critical error' + . ' as there is probably only access to the database permitted which does not exist yet. If you are' + . ' absolutely sure you are running PostgreSQL in a version equal to or newer than 9.1,' + . ' you can skip the validation and safely proceed to the next step. The error was: %s'), + $connectionError->getMessage() + )); + $state = false; + } else { + $version = $db->getServerVersion(); + if (version_compare($version, '9.1', '<')) { + $this->error(sprintf( + $this->translate('The server\'s version %s is too old. The minimum required version is %s.'), + $version, + '9.1' + )); + $state = false; + } + } + } + + return $state; + } + + /** + * Add a checkbox to the form by which the user can skip the configuration validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => $this->translate('Skip Validation'), + 'description' => $this->translate('Check this to not to validate the configuration') + ) + ); + } +} diff --git a/modules/setup/application/forms/GeneralConfigPage.php b/modules/setup/application/forms/GeneralConfigPage.php new file mode 100644 index 0000000..5b9f011 --- /dev/null +++ b/modules/setup/application/forms/GeneralConfigPage.php @@ -0,0 +1,41 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Forms\Config\General\ApplicationConfigForm; +use Icinga\Forms\Config\General\LoggingConfigForm; +use Icinga\Web\Form; + +/** + * Wizard page to define the application and logging configuration + */ +class GeneralConfigPage extends Form +{ + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_general_config'); + $this->setTitle($this->translate('Application Configuration', 'setup.page.title')); + $this->addDescription($this->translate( + 'Now please adjust all application and logging related configuration options to fit your needs.' + )); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $appConfigForm = new ApplicationConfigForm(); + $appConfigForm->createElements($formData); + $appConfigForm->removeElement('global_module_path'); + $appConfigForm->removeElement('global_config_resource'); + $this->addElements($appConfigForm->getElements()); + + $loggingConfigForm = new LoggingConfigForm(); + $this->addElements($loggingConfigForm->createElements($formData)->getElements()); + } +} diff --git a/modules/setup/application/forms/LdapDiscoveryConfirmPage.php b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php new file mode 100644 index 0000000..33bc907 --- /dev/null +++ b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php @@ -0,0 +1,133 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Data\ConfigObject; +use Icinga\Web\Form; + +/** + * Wizard page to define the connection details for a LDAP resource + */ +class LdapDiscoveryConfirmPage extends Form +{ + const TYPE_AD = 'MS ActiveDirectory'; + const TYPE_MISC = 'LDAP'; + + private $infoTemplate = <<< 'EOT' +<table><tbody> + <tr><td><strong>Type:</strong></td><td>{type}</td></tr> + <tr><td><strong>Port:</strong></td><td>{port}</td></tr> + <tr><td><strong>Root DN:</strong></td><td>{root_dn}</td></tr> + <tr><td><strong>User Object Class:</strong></td><td>{user_class}</td></tr> + <tr><td><strong>User Name Attribute:</strong></td><td>{user_attribute}</td></tr> +</tbody></table> +EOT; + + /** + * The previous configuration + * + * @var array + */ + private $config; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_ldap_discovery_confirm'); + $this->setTitle($this->translate('LDAP Discovery Results', 'setup.page.title')); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return $this + */ + public function setResourceConfig(array $config) + { + $this->config = $config; + return $this; + } + + /** + * Return the resource configuration as Config object + * + * @return ConfigObject + */ + public function getResourceConfig() + { + return new ConfigObject($this->config); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $resource = $this->config['resource']; + $backend = $this->config['backend']; + $html = $this->infoTemplate; + $html = str_replace('{type}', $this->config['type'], $html); + $html = str_replace('{hostname}', $resource['hostname'], $html); + $html = str_replace('{port}', $resource['port'], $html); + $html = str_replace('{root_dn}', $resource['root_dn'], $html); + $html = str_replace('{user_attribute}', $backend['user_name_attribute'], $html); + $html = str_replace('{user_class}', $backend['user_class'], $html); + + $this->addDescription(sprintf( + $this->translate('The following directory service has been found on domain "%s".'), + $this->config['domain'] + )); + + $this->addElement( + 'note', + 'suggestion', + array( + 'value' => $html, + 'decorators' => array( + 'ViewHelper', + array( + 'HtmlTag', array('tag' => 'div') + ) + ) + ) + ); + + $this->addElement( + 'checkbox', + 'confirm', + array( + 'value' => '1', + 'label' => $this->translate('Use this configuration?') + ) + ); + } + + /** + * Validate the given form data and check whether a BIND-request is successful + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + return true; + } + + public function getValues($suppressArrayNotation = false) + { + if ($this->getValue('confirm') === '1') { + // use configuration + return $this->config; + } + return null; + } +} diff --git a/modules/setup/application/forms/LdapDiscoveryPage.php b/modules/setup/application/forms/LdapDiscoveryPage.php new file mode 100644 index 0000000..7b5de17 --- /dev/null +++ b/modules/setup/application/forms/LdapDiscoveryPage.php @@ -0,0 +1,115 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Exception; +use Zend_Validate_NotEmpty; +use Icinga\Exception\IcingaException; +use Icinga\Web\Form; +use Icinga\Web\Form\ErrorLabeller; +use Icinga\Forms\LdapDiscoveryForm; +use Icinga\Protocol\Ldap\Discovery; +use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage; + +/** + * Wizard page to define the connection details for a LDAP resource + */ +class LdapDiscoveryPage extends Form +{ + /** + * @var Discovery + */ + private $discovery; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_ldap_discovery'); + $this->setTitle($this->translate('LDAP Discovery', 'setup.page.title')); + $this->addDescription($this->translate( + 'You can use this page to discover LDAP or ActiveDirectory servers ' . + ' for authentication. If you don\'t want to execute a discovery, just skip this step.' + )); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $discoveryForm = new LdapDiscoveryForm(); + $this->addElements($discoveryForm->createElements($formData)->getElements()); + + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'label' => $this->translate('Skip'), + 'description' => $this->translate('Do not discover LDAP servers and enter all settings manually.') + ) + ); + } + + /** + * Validate the given form data and check whether a BIND-request is successful + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + if (isset($data['skip_validation']) && $data['skip_validation']) { + return true; + } + + if (isset($data['domain']) && $data['domain']) { + try { + $this->discovery = Discovery::discoverDomain($data['domain']); + if ($this->discovery->isSuccess()) { + return true; + } else { + $this->error($this->discovery->getError()->getMessage()); + } + } catch (Exception $e) { + $this->error(sprintf( + $this->translate('Could not find any LDAP servers on the domain "%s". An error occurred: %s'), + $data['domain'], + IcingaException::describe($e) + )); + } + } else { + $labeller = new ErrorLabeller(array('element' => $this->getElement('domain'))); + $this->getElement('domain')->addError($labeller->translate(Zend_Validate_NotEmpty::IS_EMPTY)); + } + + return false; + } + + /** + * Suggest settings based on the underlying discovery + * + * @param bool $suppressArrayNotation + * + * @return array + */ + public function getValues($suppressArrayNotation = false) + { + if (! isset($this->discovery) || ! $this->discovery->isSuccess()) { + return []; + } + $disc = $this->discovery; + return array( + 'domain' => $this->getValue('domain'), + 'type' => $disc->isAd() ? LdapDiscoveryConfirmPage::TYPE_AD : LdapDiscoveryConfirmPage::TYPE_MISC, + 'resource' => $disc->suggestResourceSettings(), + 'backend' => $disc->suggestBackendSettings() + ); + } +} diff --git a/modules/setup/application/forms/LdapResourcePage.php b/modules/setup/application/forms/LdapResourcePage.php new file mode 100644 index 0000000..7786407 --- /dev/null +++ b/modules/setup/application/forms/LdapResourcePage.php @@ -0,0 +1,152 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Web\Form; +use Icinga\Forms\Config\ResourceConfigForm; +use Icinga\Forms\Config\Resource\LdapResourceForm; + +/** + * Wizard page to define the connection details for a LDAP resource + */ +class LdapResourcePage extends Form +{ + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_ldap_resource'); + $this->setTitle($this->translate('LDAP Resource', 'setup.page.title')); + $this->addDescription($this->translate( + 'Now please configure your AD/LDAP resource. This will later ' + . 'be used to authenticate users logging in to Icinga Web 2.' + )); + $this->setValidatePartial(true); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'ldap' + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $resourceForm = new LdapResourceForm(); + $this->addElements($resourceForm->createElements($formData)->getElements()); + $this->getElement('name')->setValue('icingaweb_ldap'); + } + + /** + * Validate the given form data and check whether a BIND-request is successful + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (! parent::isValid($data)) { + return false; + } + + if (! isset($data['skip_validation']) || $data['skip_validation'] == 0) { + $inspection = ResourceConfigForm::inspectResource($this); + if ($inspection !== null && $inspection->hasError()) { + $this->error($inspection->getError()); + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Run the configured backend's inspection checks and show the result, if necessary + * + * This will only run any validation if the user pushed the 'backend_validation' button. + * + * @param array $formData + * + * @return bool + */ + public function isValidPartial(array $formData) + { + if (isset($formData['backend_validation']) && parent::isValid($formData)) { + $inspection = ResourceConfigForm::inspectResource($this); + if ($inspection !== null) { + $join = function ($e) use (&$join) { + return is_string($e) ? $e : join("\n", array_map($join, $e)); + }; + $this->addElement( + 'note', + 'inspection_output', + array( + 'order' => 0, + 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n" + . join("\n", array_map($join, $inspection->toArray())), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')), + ) + ) + ); + + if ($inspection->hasError()) { + $this->warning(sprintf( + $this->translate('Failed to successfully validate the configuration: %s'), + $inspection->getError() + )); + return false; + } + } + + $this->info($this->translate('The configuration has been successfully validated.')); + } elseif (! isset($formData['backend_validation'])) { + // This is usually done by isValid(Partial), but as we're not calling any of these... + $this->populate($formData); + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => $this->translate('Skip Validation'), + 'description' => $this->translate( + 'Check this to not to validate connectivity with the given directory service' + ) + ) + ); + } +} diff --git a/modules/setup/application/forms/ModulePage.php b/modules/setup/application/forms/ModulePage.php new file mode 100644 index 0000000..d62b5a9 --- /dev/null +++ b/modules/setup/application/forms/ModulePage.php @@ -0,0 +1,108 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; +use Icinga\Web\Form; + +class ModulePage extends Form +{ + protected $modules; + + protected $modulePaths; + + protected $foundIcingaDB = false; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_modules'); + $this->setViewScript('form/setup-modules.phtml'); + + $this->modulePaths = array(); + if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) { + $this->modulePaths[] = $appModulePath; + } + } + + public function createElements(array $formData) + { + foreach ($this->getModules() as $module) { + $checked = false; + if ($module->getName() === 'monitoring') { + $checked = ! $this->foundIcingaDB; + } elseif ($this->foundIcingaDB && $module->getName() === 'icingadb') { + $checked = true; + } + + $this->addElement( + 'checkbox', + $module->getName(), + array( + 'description' => $module->getDescription(), + 'label' => ucfirst($module->getName()), + 'value' => (int) $checked, + 'decorators' => array('ViewHelper') + ) + ); + } + } + + /** + * @return Module[] + */ + protected function getModules() + { + if ($this->modules !== null) { + return $this->modules; + } else { + $this->modules = array(); + } + + $moduleManager = Icinga::app()->getModuleManager(); + $moduleManager->detectInstalledModules($this->modulePaths); + foreach ($moduleManager->listInstalledModules() as $moduleName) { + if ($moduleName !== 'setup') { + $this->modules[$moduleName] = $moduleManager->loadModule($moduleName)->getModule($moduleName); + } + + if ($moduleName === 'icingadb') { + $this->foundIcingaDB = true; + } + } + + return $this->modules; + } + + public function getCheckedModules() + { + $modules = $this->getModules(); + + $checked = array(); + foreach ($this->getElements() as $name => $element) { + if (array_key_exists($name, $modules) && $element->isChecked()) { + $checked[$name] = $modules[$name]; + } + } + + return $checked; + } + + public function getModuleWizards() + { + $checked = $this->getCheckedModules(); + + $wizards = array(); + foreach ($checked as $name => $module) { + if ($module->providesSetupWizard()) { + $wizards[$name] = $module->getSetupWizard(); + } + } + + return $wizards; + } +} diff --git a/modules/setup/application/forms/RequirementsPage.php b/modules/setup/application/forms/RequirementsPage.php new file mode 100644 index 0000000..d1fb70e --- /dev/null +++ b/modules/setup/application/forms/RequirementsPage.php @@ -0,0 +1,68 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Web\Form; +use Icinga\Module\Setup\SetupWizard; + +/** + * Wizard page to list setup requirements + */ +class RequirementsPage extends Form +{ + /** + * The wizard + * + * @var SetupWizard + */ + protected $wizard; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_requirements'); + $this->setViewScript('form/setup-requirements.phtml'); + } + + /** + * Set the wizard + * + * @param SetupWizard $wizard + * + * @return $this + */ + public function setWizard(SetupWizard $wizard) + { + $this->wizard = $wizard; + return $this; + } + + /** + * Return the wizard + * + * @return SetupWizard + */ + public function getWizard() + { + return $this->wizard; + } + + /** + * Validate the given form data and check whether the wizard's requirements are fulfilled + * + * @param array $data The data to validate + * + * @return bool + */ + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + return $this->wizard->getRequirements()->fulfilled(); + } +} diff --git a/modules/setup/application/forms/SummaryPage.php b/modules/setup/application/forms/SummaryPage.php new file mode 100644 index 0000000..ab62d55 --- /dev/null +++ b/modules/setup/application/forms/SummaryPage.php @@ -0,0 +1,84 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use LogicException; +use Icinga\Web\Form; + +/** + * Wizard page that displays a summary of what is going to be "done" + */ +class SummaryPage extends Form +{ + /** + * The title of what is being set up + * + * @var string + */ + protected $title; + + /** + * The summary to show + * + * @var array + */ + protected $summary; + + /** + * Initialize this page + */ + public function init() + { + if ($this->getName() === $this->filterName(get_class($this))) { + throw new LogicException( + 'When utilizing ' . get_class($this) . ' it is required to set a unique name by using the form options' + ); + } + + $this->setViewScript('form/setup-summary.phtml'); + } + + /** + * Set the title of what is being set up + * + * @param string $title + */ + public function setSubjectTitle($title) + { + $this->title = $title; + } + + /** + * Return the title of what is being set up + * + * @return string + */ + public function getSubjectTitle() + { + return $this->title; + } + + /** + * Set the summary to show + * + * @param array $summary + * + * @return $this + */ + public function setSummary(array $summary) + { + $this->summary = $summary; + return $this; + } + + /** + * Return the summary to show + * + * @return array + */ + public function getSummary() + { + return $this->summary; + } +} diff --git a/modules/setup/application/forms/UserGroupBackendPage.php b/modules/setup/application/forms/UserGroupBackendPage.php new file mode 100644 index 0000000..751270f --- /dev/null +++ b/modules/setup/application/forms/UserGroupBackendPage.php @@ -0,0 +1,147 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Application\Config; +use Icinga\Authentication\User\UserBackend; +use Icinga\Data\ResourceFactory; +use Icinga\Forms\Config\UserGroup\LdapUserGroupBackendForm; +use Icinga\Web\Form; + +/** + * Wizard page to define user group backend specific details + */ +class UserGroupBackendPage extends Form +{ + /** + * The resource configuration to use + * + * @var array + */ + protected $resourceConfig; + + /** + * The user backend configuration to use + * + * @var array + */ + protected $backendConfig; + + /** + * Initialize this page + */ + public function init() + { + $this->setName('setup_usergroup_backend'); + $this->setTitle($this->translate('User Group Backend', 'setup.page.title')); + $this->addDescription($this->translate( + 'To allow Icinga Web 2 to associate users and groups, you\'ll need to provide some further information' + . ' about the LDAP Connection that is already going to be used to locate account details.' + )); + } + + /** + * Set the resource configuration to use + * + * @param array $config + * + * @return $this + */ + public function setResourceConfig(array $config) + { + $this->resourceConfig = $config; + return $this; + } + + /** + * Set the user backend configuration to use + * + * @param array $config + * + * @return $this + */ + public function setBackendConfig(array $config) + { + $this->backendConfig = $config; + return $this; + } + + /** + * Return the resource configuration as Config object + * + * @return Config + */ + protected function createResourceConfiguration() + { + $config = new Config(); + $config->setSection($this->resourceConfig['name'], $this->resourceConfig); + return $config; + } + + /** + * Return the user backend configuration as Config object + * + * @return Config + */ + protected function createBackendConfiguration() + { + $config = new Config(); + $backendConfig = $this->backendConfig; + $backendConfig['resource'] = $this->resourceConfig['name']; + $config->setSection($this->backendConfig['name'], $backendConfig); + return $config; + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + // LdapUserGroupBackendForm requires these factories to provide valid configurations + ResourceFactory::setConfig($this->createResourceConfiguration()); + UserBackend::setConfig($this->createBackendConfiguration()); + + $backendForm = new LdapUserGroupBackendForm(); + $formData['type'] = 'ldap'; + $backendForm->create($formData); + $backendForm->getElement('name')->setValue('icingaweb2'); + $this->addSubForm($backendForm, 'backend_form'); + + $backendForm->addElement( + 'hidden', + 'resource', + array( + 'required' => true, + 'value' => $this->resourceConfig['name'], + 'decorators' => array('ViewHelper') + ) + ); + $backendForm->addElement( + 'hidden', + 'user_backend', + array( + 'required' => true, + 'value' => $this->backendConfig['name'], + 'decorators' => array('ViewHelper') + ) + ); + } + + /** + * Retrieve all form element values + * + * @param bool $suppressArrayNotation Ignored + * + * @return array + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues(); + $values = array_merge($values, $values['backend_form']); + unset($values['backend_form']); + return $values; + } +} diff --git a/modules/setup/application/forms/WelcomePage.php b/modules/setup/application/forms/WelcomePage.php new file mode 100644 index 0000000..124a31f --- /dev/null +++ b/modules/setup/application/forms/WelcomePage.php @@ -0,0 +1,45 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Forms; + +use Icinga\Application\Icinga; +use Icinga\Web\Form; +use Icinga\Module\Setup\Web\Form\Validator\TokenValidator; + +/** + * Wizard page to authenticate and welcome the user + */ +class WelcomePage extends Form +{ + /** + * Initialize this page + */ + public function init() + { + $this->setRequiredCue(null); + $this->setName('setup_welcome'); + $this->setViewScript('form/setup-welcome.phtml'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'token', + array( + 'class' => 'autofocus', + 'required' => true, + 'label' => $this->translate('Setup Token'), + 'description' => $this->translate( + 'For security reasons we need to ensure that you are permitted to run this wizard.' + . ' Please provide a token by following the instructions below.' + ), + 'validators' => array(new TokenValidator(Icinga::app()->getConfigDir() . '/setup.token')) + ) + ); + } +} diff --git a/modules/setup/application/views/scripts/form/setup-modules.phtml b/modules/setup/application/views/scripts/form/setup-modules.phtml new file mode 100644 index 0000000..e57c7dc --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-modules.phtml @@ -0,0 +1,33 @@ +<?php + +use Icinga\Web\Wizard; + +?> +<form + id="<?= $this->escape($form->getName()); ?>" + name="<?= $this->escape($form->getName()); ?>" + enctype="<?= $this->escape($form->getEncType()); ?>" + method="<?= $this->escape($form->getMethod()); ?>" + action="<?= $this->escape($form->getAction()); ?>" + class="icinga-controls" + data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>" +> +<h2><?= $this->translate('Modules', 'setup.page.title'); ?></h2> +<p><?= $this->translate('The following modules were found in your Icinga Web 2 installation. To enable and configure a module, just tick it and click "Next".'); ?></p> +<?php foreach ($form->getElements() as $element): ?> + <?php if (! in_array($element->getName(), array(Wizard::BTN_PREV, Wizard::BTN_NEXT, Wizard::PROGRESS_ELEMENT, $form->getTokenElementName(), $form->getUidElementName()))): ?> + <div class="module"> + <div class="header"> + <h3><label for="<?= $element->getId(); ?>"><strong><?= $element->getLabel(); ?></strong></label></h3> + <div class="element"> + <?= $element; ?> + </div> + </div> + <label class="description" for="<?= $element->getId(); ?>"><?= $element->getDescription(); ?></label> + </div> + <?php endif ?> +<?php endforeach ?> + <?= $form->getElement($form->getTokenElementName()); ?> + <?= $form->getElement($form->getUidElementName()); ?> + <?= $form->getDisplayGroup('buttons'); ?> +</form> diff --git a/modules/setup/application/views/scripts/form/setup-requirements.phtml b/modules/setup/application/views/scripts/form/setup-requirements.phtml new file mode 100644 index 0000000..544f284 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-requirements.phtml @@ -0,0 +1,48 @@ +<?php + +use Icinga\Web\Wizard; + +if (! $form->getWizard()->getRequirements()->fulfilled()) { + $form->getElement(Wizard::BTN_NEXT)->setAttrib('disabled', 1); +} + +?> +<h1>Icinga Web 2</h1> +<?= $form->getWizard()->getRequirements(true); ?> +<?php foreach ($form->getWizard()->getPage('setup_modules')->getModuleWizards() as $moduleName => $wizard): ?> +<h1><?= ucwords($moduleName) . ' ' . $this->translate('Module'); ?></h1> +<?= $wizard->getRequirements(); ?> +<?php endforeach ?> +<form + id="<?= $this->escape($form->getName()); ?>" + name="<?= $this->escape($form->getName()); ?>" + enctype="<?= $this->escape($form->getEncType()); ?>" + method="<?= $this->escape($form->getMethod()); ?>" + action="<?= $this->escape($form->getAction()); ?>" + data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>" +> + <?= $form->getElement($form->getTokenElementName()); ?> + <?= $form->getElement($form->getUidElementName()); ?> + <div class="buttons"> + <?php + $double = clone $form->getElement(Wizard::BTN_NEXT); + echo $double->setAttrib('class', 'double'); + ?> + <?= $form->getElement(Wizard::BTN_PREV); ?> + <?= $form->getElement(Wizard::BTN_NEXT); ?> + <?= $form->getElement(Wizard::PROGRESS_ELEMENT); ?> + <div class="requirements-refresh"> + <?php $title = $this->translate('You may also need to restart the web-server for the changes to take effect!'); ?> + <?= $this->qlink( + $this->translate('Refresh'), + null, + null, + array( + 'class' => 'button-link', + 'title' => $title, + 'aria-label' => sprintf($this->translate('Refresh the page; %s'), $title) + ) + ); ?> + </div> + </div> +</form>
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/form/setup-summary.phtml b/modules/setup/application/views/scripts/form/setup-summary.phtml new file mode 100644 index 0000000..3ad0265 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-summary.phtml @@ -0,0 +1,40 @@ +<?php + +use Icinga\Web\Wizard; + +$form->getElement(Wizard::BTN_NEXT)->setAttrib( + 'class', + $form->getElement(Wizard::BTN_NEXT)->getAttrib('class') . ' finish' +); + +?> +<p><?= sprintf( + $this->translate( + 'You\'ve configured %1$s successfully. You can review the changes supposed to be made before setting it up.' + . ' Make sure that everything is correct (Feel free to navigate back to make any corrections!) so' + . ' that you can start using %1$s right after it has successfully been set up.' + ), + $form->getSubjectTitle() +); ?></p> +<div class="summary"> +<?php foreach ($form->getSummary() as $pageHtml): ?> + <?php if ($pageHtml): ?> + <div class="page"> + <?= $pageHtml; ?> + </div> + <?php endif ?> +<?php endforeach ?> +</div> +<form + id="<?= $this->escape($form->getName()); ?>" + name="<?= $this->escape($form->getName()); ?>" + enctype="<?= $this->escape($form->getEncType()); ?>" + method="<?= $this->escape($form->getMethod()); ?>" + action="<?= $this->escape($form->getAction()); ?>" + data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>" + class="summary" +> + <?= $form->getElement($form->getTokenElementName()); ?> + <?= $form->getElement($form->getUidElementName()); ?> + <?= $form->getDisplayGroup('buttons'); ?> +</form>
\ No newline at end of file diff --git a/modules/setup/application/views/scripts/form/setup-welcome.phtml b/modules/setup/application/views/scripts/form/setup-welcome.phtml new file mode 100644 index 0000000..1be68f3 --- /dev/null +++ b/modules/setup/application/views/scripts/form/setup-welcome.phtml @@ -0,0 +1,120 @@ +<?php + +use Icinga\Application\Icinga; +use Icinga\Application\Config; +use Icinga\Application\Platform; +use Icinga\Web\Wizard; + +$phpUser = Platform::getPhpUser(); +$configDir = Icinga::app()->getConfigDir(); +$setupTokenPath = rtrim($configDir, '/') . '/setup.token'; +$cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli'); + +$groupadd = null; +$docker = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE'); + +if (! (false === ($distro = Platform::getLinuxDistro(1)) || $distro === 'linux')) { + foreach (array( + 'groupadd -r icingaweb2' => array( + 'redhat', 'rhel', 'centos', 'fedora', + 'suse', 'sles', 'sled', 'opensuse' + ), + 'addgroup --system icingaweb2' => array('debian', 'ubuntu') + ) as $groupadd_ => $distros) { + if (in_array($distro, $distros)) { + $groupadd = $groupadd_; + break; + } + } + + switch ($distro) { + case 'redhat': + case 'rhel': + case 'centos': + case 'fedora': + $usermod = 'usermod -a -G icingaweb2 %s'; + $webSrvUser = 'apache'; + break; + case 'suse': + case 'sles': + case 'sled': + case 'opensuse': + $usermod = 'usermod -A icingaweb2 %s'; + $webSrvUser = 'wwwrun'; + break; + case 'debian': + case 'ubuntu': + $usermod = 'usermod -a -G icingaweb2 %s'; + $webSrvUser = 'www-data'; + break; + default: + $usermod = $webSrvUser = null; + } +} +?> +<div class="welcome-page"> + <h2><?= $this->translate('Welcome to the configuration of Icinga Web 2!') ?></h2> + <?php if (false === file_exists($setupTokenPath) && file_exists(Config::resolvePath('config.ini'))): ?> + <p class="restart-warning"><?= $this->translate( + 'You\'ve already completed the configuration of Icinga Web 2. Note that most of your configuration' + . ' files will be overwritten in case you\'ll re-configure Icinga Web 2 using this wizard!' + ); ?></p> + <?php else: ?> + <p><?= $this->translate( + 'This wizard will guide you through the configuration of Icinga Web 2. Once completed and successfully' + . ' finished you are able to log in and to explore all the new and stunning features!' + ); ?></p> + <?php endif ?> + <form id="<?= $form->getName(); ?>" name="<?= $form->getName(); ?>" enctype="<?= $form->getEncType(); ?>" method="<?= $form->getMethod(); ?>" action="<?= $form->getAction(); ?>" class="icinga-controls"> + <?= $form->getElement('token'); ?> + <?= $form->getElement($form->getTokenElementName()); ?> + <?= $form->getElement($form->getUidElementName()); ?> + <div class="buttons"> + <?= $form->getElement(Wizard::BTN_NEXT); ?> + </div> + </form> + <div class="note"> + <h3><?= $this->translate('Generating a New Setup Token'); ?></h3> + <div> + <p><?= + $this->translate( + 'To run this wizard a user needs to authenticate using a token which is usually' + . ' provided to him by an administrator who\'d followed the instructions below.' + ); ?></p> + <?php if (! $docker): ?> + <p><?= $this->translate('In any case, make sure that all of the following applies to your environment:'); ?></p> + <ul> + <li><?= $this->translate('A system group called "icingaweb2" exists'); ?></li> + <?php if ($phpUser): ?> + <li><?= sprintf($this->translate('The user "%s" is a member of the system group "icingaweb2"'), $phpUser); ?></li> + <?php else: ?> + <li><?= $this->translate('Your webserver\'s user is a member of the system group "icingaweb2"'); ?></li> + <?php endif ?> + </ul> + <?php if (! ($groupadd === null || $usermod === null)) { ?> + <div class="code"> + <span><?= $this->escape($groupadd . ';') ?></span> + <span><?= $this->escape(sprintf($usermod, $phpUser ?: $webSrvUser) . ';') ?></span> + </div> + <?php } ?> + <p><?= $this->translate('If you\'ve got the IcingaCLI installed you can do the following:'); ?></p> + <?php endif; ?> + <div class="code"> + <?php if (! $docker): ?> + <span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup config directory --group icingaweb2<?= $configDir !== '/etc/icingaweb2' ? ' --config ' . $configDir : ''; ?>;</span> + <?php endif; ?> + <span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup token create;</span> + </div> + <?php if (! $docker): ?> + <p><?= $this->translate('In case the IcingaCLI is missing you can create the token manually:'); ?></p> + <div class="code"> + <span>su <?= $phpUser ?: $this->translate('<your-webserver-user>'); ?> -s /bin/sh -c "mkdir -m 2770 <?= dirname($setupTokenPath); ?>; chgrp icingaweb2 <?= dirname($setupTokenPath); ?>; head -c 12 /dev/urandom | base64 | tee <?= $setupTokenPath; ?>; chmod 0660 <?= $setupTokenPath; ?>;";</span> + </div> + <?php endif; ?> + <p><?= sprintf( + $this->translate('Please see the %s for an extensive description on how to access and use this wizard.'), + '<a href="http://docs.icinga.com/">' . $this->translate('Icinga Web 2 documentation') . '</a>' // TODO: Add link to iw2 docs which points to the installation topic + ); ?></p> + </div> + </div> +</div> diff --git a/modules/setup/application/views/scripts/index/index.phtml b/modules/setup/application/views/scripts/index/index.phtml new file mode 100644 index 0000000..b2b3bda --- /dev/null +++ b/modules/setup/application/views/scripts/index/index.phtml @@ -0,0 +1,153 @@ +<?php + +use Icinga\Web\Notification; + +$pages = $wizard->getPages(); +$finished = isset($success); +$configPages = array_slice($pages, 3, count($pages) - 4, true); +$currentPos = array_search($wizard->getCurrentPage(), $pages, true); +list($configPagesLeft, $configPagesRight) = array_chunk($configPages, (int)(count($configPages) / 2), true); + +$visitedPages = array_keys($wizard->getPageData()); +$maxProgress = max(array_merge([0], array_keys(array_filter( + $pages, + function ($page) use ($visitedPages) { return in_array($page->getName(), $visitedPages); } +)))); + +?> +<div id="setup-content-wrapper" data-base-target="layout"> + <div class="setup-header"> + <?= $this->img('img/icinga-logo-big.png'); ?> + <div class="progress-bar"> + <div class="step" style="width: 10%;"> + <h1><?= $this->translate('Welcome', 'setup.progress'); ?></h1> + <?php $stateClass = $finished || $currentPos > 0 ? 'complete' : ( + $maxProgress > 0 ? 'visited' : 'active' + ); ?> + <table><tbody><tr> + <td class="left"></td> + <td class="middle"><div class="bubble <?= $stateClass; ?>"></div></td> + <td class="right"><div class="line right <?= $stateClass; ?>"></div></td> + </tr></tbody></table> + </div> + <div class="step" style="width: 10%;"> + <h1><?= $this->translate('Modules', 'setup.progress'); ?></h1> + <?php $stateClass = $finished || $currentPos > 1 ? ' complete' : ( + $maxProgress > 1 ? ' visited' : ( + $currentPos === 1 ? ' active' : '' + ) + ); ?> + <table><tbody><tr> + <td class="left"><div class="line left<?= $stateClass; ?>"></div></td> + <td class="middle"><div class="bubble <?= $stateClass; ?>"></div></td> + <td class="right"><div class="line right <?= $stateClass; ?>"></div></td> + </tr></tbody></table> + <?php if (($maxProgress < $currentPos && $currentPos === 1) || ($maxProgress >= $currentPos && $maxProgress === 1)): ?> + <?= $this->restartForm ?> + <?php endif ?> + </div> + <div class="step" style="width: 10%;"> + <h1><?= $this->translate('Requirements', 'setup.progress'); ?></h1> + <?php $stateClass = $finished || $currentPos > 2 ? ' complete' : ( + $maxProgress > 2 ? ' visited' : ( + $currentPos === 2 ? ' active' : '' + ) + ); ?> + <table><tbody><tr> + <td class="left"><div class="line left<?= $stateClass; ?>"></div></td> + <td class="middle"><div class="bubble<?= $stateClass; ?>"></div></td> + <td class="right"><div class="line right<?= $stateClass; ?>"></div></td> + </tr></tbody></table> + <?php if (($maxProgress < $currentPos && $currentPos === 2) || ($maxProgress >= $currentPos && $maxProgress === 2)): ?> + <?= $this->restartForm ?> + <?php endif ?> + </div> + <div class="step" style="width: 60%;"> + <h1><?= $this->translate('Configuration', 'setup.progress'); ?></h1> + <table><tbody><tr> + <td class="left"> + <?php + $firstPage = current($configPagesLeft); + $lastPage = end($configPagesLeft); + $lineWidth = sprintf('%.2F', round(100 / count($configPagesLeft), 2, PHP_ROUND_HALF_DOWN)); + ?> + <?php foreach ($configPagesLeft as $pos => $page): ?> + <?php $stateClass = $finished || $pos < $currentPos ? ' complete' : ( + $pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '') + ); ?> + <?php if ($page === $firstPage): ?> + <div class="line left<?= $stateClass; ?>" style="float: left; width: <?= sprintf( + '%.2F', + 100 - (count($configPagesLeft) - 1) * $lineWidth + ); ?>%; margin-right: 0"></div> + <?php elseif ($page === $lastPage): ?> + <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-right: -0.1em;"></div> + <?php else: ?> + <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></div> + <?php endif ?> + <?php endforeach ?> + </td> + <td class="middle"> + <div class="bubble<?= array_key_exists($currentPos, $configPagesLeft) ? ( + key($configPagesRight) <= $maxProgress ? ' visited' : ' active') : ( + $finished || $currentPos > 2 ? ' complete' : ( + key($configPagesRight) < $maxProgress ? ' visited' : '' + ) + ); ?>"></div> + </td> + <td class="right"> + <?php + $firstPage = current($configPagesRight); + $lastPage = end($configPagesRight); + $lineWidth = sprintf('%.2F', round(100 / count($configPagesRight), 2, PHP_ROUND_HALF_DOWN)); + ?> + <?php foreach ($configPagesRight as $pos => $page): ?> + <?php $stateClass = $finished || $pos < $currentPos ? ' complete' : ( + $pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '') + ); ?> + <?php if ($page === $firstPage): ?> + <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-left: -0.1em;"></div> + <?php elseif ($page === $lastPage): ?> + <div class="line right<?= $stateClass; ?>" style="float: left; width: <?= sprintf( + '%.2F', + 100 - (count($configPagesRight) - 1) * $lineWidth + ); ?>%; margin-left: 0;"></div> + <?php else: ?> + <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></div> + <?php endif ?> + <?php endforeach ?> + </td> + </tr></tbody></table> + <?php if ($maxProgress > 2 || $currentPos > 2): ?> + <?= $this->restartForm ?> + <?php endif ?> + </div> + <div class="step" style="width: 10%;"> + <h1><?= $this->translate('Finish', 'setup.progress'); ?></h1> + <?php $stateClass = $finished ? ' complete' : ($pages[$currentPos] === end($pages) ? ' active' : ''); ?> + <table><tbody><tr> + <td class="left"><div class="line left<?= $stateClass; ?>"></div></td> + <td class="middle"><div class="bubble<?= $stateClass; ?>"></div></td> + <td class="right"></td> + </tr></tbody></table> + </div> + </div> + </div> + <div class="setup-content"> +<?php if ($finished): ?> + <?= $this->render('index/parts/finish.phtml'); ?> +<?php else: ?> + <?= $this->render('index/parts/wizard.phtml'); ?> +<?php endif ?> + </div> +</div> +<div id="footer"> + <ul role="alert" id="notifications"><?php + $notifications = Notification::getInstance(); + if ($notifications->hasMessages()) { + foreach ($notifications->popMessages() as $m) { + echo '<li class="' . $m->type . '">' . $this->escape($m->message) . '</li>'; + } + } + ?></ul> +</div> diff --git a/modules/setup/application/views/scripts/index/parts/finish.phtml b/modules/setup/application/views/scripts/index/parts/finish.phtml new file mode 100644 index 0000000..dc5ba1c --- /dev/null +++ b/modules/setup/application/views/scripts/index/parts/finish.phtml @@ -0,0 +1,34 @@ +<div id="setup-finish"> + <?php if ($success): ?> + <h2 class="success"><?= $this->translate('Congratulations! Icinga Web 2 has been successfully set up.'); ?></h2> + <?php else: ?> + <h2 class="failure"><?= $this->translate('Sorry! Failed to set up Icinga Web 2 successfully.'); ?></h2> + <?php endif ?> + <div class="buttons pull-right"> + <?php if ($success): ?> + <?= $this->qlink( + $this->translate('Login to Icinga Web 2'), + 'authentication/login', + null, + array( + 'class' => 'button-link login', + 'data-no-icinga-ajax' => true, + 'title' => $this->translate('Show the login page of Icinga Web 2') + ) + ); ?> + <?php else: ?> + <?= $this->qlink( + $this->translate('Back'), + null, + null, + array( + 'class' => 'button-link', + 'title' => $this->translate('Show previous wizard-page') + ) + ); ?> + <?php endif ?> + </div> + <pre class="log-output"><?= join("\n\n", array_map(function($a) { + return join("\n", $a); + }, $report)); ?></pre> +</div> diff --git a/modules/setup/application/views/scripts/index/parts/wizard.phtml b/modules/setup/application/views/scripts/index/parts/wizard.phtml new file mode 100644 index 0000000..94891f9 --- /dev/null +++ b/modules/setup/application/views/scripts/index/parts/wizard.phtml @@ -0,0 +1 @@ +<?= $wizard->getForm()->render(); ?>
\ No newline at end of file diff --git a/modules/setup/library/Setup/Exception/SetupException.php b/modules/setup/library/Setup/Exception/SetupException.php new file mode 100644 index 0000000..c3ae591 --- /dev/null +++ b/modules/setup/library/Setup/Exception/SetupException.php @@ -0,0 +1,22 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Exception; + +use Icinga\Exception\IcingaException; + +/** + * Class SetupException + * + * Used to indicate that a setup should be aborted. + */ +class SetupException extends IcingaException +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + parent::__construct('Setup abortion'); + } +} diff --git a/modules/setup/library/Setup/Requirement.php b/modules/setup/library/Setup/Requirement.php new file mode 100644 index 0000000..fd16405 --- /dev/null +++ b/modules/setup/library/Setup/Requirement.php @@ -0,0 +1,343 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use LogicException; + +abstract class Requirement +{ + /** + * The state of this requirement + * + * @var bool + */ + protected $state; + + /** + * A descriptive text representing the current state of this requirement + * + * @var string + */ + protected $stateText; + + /** + * The descriptions of this requirement + * + * @var array + */ + protected $descriptions; + + /** + * The title of this requirement + * + * @var string + */ + protected $title; + + /** + * The condition of this requirement + * + * @var mixed + */ + protected $condition; + + /** + * Whether this requirement is optional + * + * @var bool + */ + protected $optional; + + /** + * The alias to display the condition with in a human readable way + * + * @var string + */ + protected $alias; + + /** + * The text to display if the given requirement is fulfilled + * + * @var string + */ + protected $textAvailable; + + /** + * The text to display if the given requirement is not fulfilled + * + * @var string + */ + protected $textMissing; + + /** + * Create a new requirement + * + * @param array $options + * + * @throws LogicException In case there exists no setter for an option's key + */ + public function __construct(array $options = array()) + { + $this->optional = false; + $this->descriptions = array(); + + foreach ($options as $key => $value) { + $setMethod = 'set' . ucfirst($key); + $addMethod = 'add' . ucfirst($key); + if (method_exists($this, $setMethod)) { + $this->$setMethod($value); + } elseif (method_exists($this, $addMethod)) { + $this->$addMethod($value); + } else { + throw LogicException('No setter found for option key: ' . $key); + } + } + } + + /** + * Set the state of this requirement + * + * @param bool $state + * + * @return Requirement + */ + public function setState($state) + { + $this->state = (bool) $state; + return $this; + } + + /** + * Return the state of this requirement + * + * Evaluates the requirement in case there is no state set yet. + * + * @return int + */ + public function getState() + { + if ($this->state === null) { + $this->state = $this->evaluate(); + } + + return $this->state; + } + + /** + * Set a descriptive text for this requirement's current state + * + * @param string $text + * + * @return Requirement + */ + public function setStateText($text) + { + $this->stateText = $text; + return $this; + } + + /** + * Return a descriptive text for this requirement's current state + * + * @return string + */ + public function getStateText() + { + $state = $this->getState(); + if ($this->stateText === null) { + return $state ? $this->getTextAvailable() : $this->getTextMissing(); + } + return $this->stateText; + } + + /** + * Add a description for this requirement + * + * @param string $description + * + * @return Requirement + */ + public function addDescription($description) + { + $this->descriptions[] = $description; + return $this; + } + + /** + * Return the descriptions of this wizard + * + * @return array + */ + public function getDescriptions() + { + return $this->descriptions; + } + + /** + * Set the title for this requirement + * + * @param string $title + * + * @return Requirement + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Return the title of this requirement + * + * In case there is no title set the alias is returned instead. + * + * @return string + */ + public function getTitle() + { + if ($this->title === null) { + return $this->getAlias(); + } + + return $this->title; + } + + /** + * Set the condition for this requirement + * + * @param mixed $condition + * + * @return Requirement + */ + public function setCondition($condition) + { + $this->condition = $condition; + return $this; + } + + /** + * Return the condition of this requirement + * + * @return mixed + */ + public function getCondition() + { + return $this->condition; + } + + /** + * Set whether this requirement is optional + * + * @param bool $state + * + * @return Requirement + */ + public function setOptional($state = true) + { + $this->optional = (bool) $state; + return $this; + } + + /** + * Return whether this requirement is optional + * + * @return bool + */ + public function isOptional() + { + return $this->optional; + } + + /** + * Set the alias to display the condition with in a human readable way + * + * @param string $alias + * + * @return Requirement + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + + /** + * Return the alias to display the condition with in a human readable way + * + * @return string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Set the text to display if the given requirement is fulfilled + * + * @param string $textAvailable + * + * @return Requirement + */ + public function setTextAvailable($textAvailable) + { + $this->textAvailable = $textAvailable; + return $this; + } + + /** + * Get the text to display if the given requirement is fulfilled + * + * @return string + */ + public function getTextAvailable() + { + return $this->textAvailable; + } + + /** + * Set the text to display if the given requirement is not fulfilled + * + * @param string $textMissing + * + * @return Requirement + */ + public function setTextMissing($textMissing) + { + $this->textMissing = $textMissing; + return $this; + } + + /** + * Get the text to display if the given requirement is not fulfilled + * + * @return string + */ + public function getTextMissing() + { + return $this->textMissing; + } + + /** + * Evaluate this requirement and return whether it is fulfilled + * + * @return bool + */ + abstract protected function evaluate(); + + /** + * Return whether the given requirement equals this one + * + * @param Requirement $requirement + * + * @return bool + */ + public function equals(Requirement $requirement) + { + if ($requirement instanceof static) { + return $this->getCondition() === $requirement->getCondition(); + } + + return false; + } +} diff --git a/modules/setup/library/Setup/Requirement/ClassRequirement.php b/modules/setup/library/Setup/Requirement/ClassRequirement.php new file mode 100644 index 0000000..d884c31 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/ClassRequirement.php @@ -0,0 +1,48 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class ClassRequirement extends Requirement +{ + protected function evaluate() + { + return Platform::classExists($this->getCondition()); + } + + /** + * {@inheritdoc} + */ + public function getStateText() + { + $stateText = parent::getStateText(); + if ($stateText === null) { + $alias = $this->getAlias(); + if ($this->getState()) { + $stateText = $alias === null + ? sprintf( + mt('setup', 'The %s class is available.', 'setup.requirement.class'), + $this->getCondition() + ) + : sprintf( + mt('setup', 'The %s is available.', 'setup.requirement.class'), + $alias + ); + } else { + $stateText = $alias === null + ? sprintf( + mt('setup', 'The %s class is missing.', 'setup.requirement.class'), + $this->getCondition() + ) + : sprintf( + mt('setup', 'The %s is missing.', 'setup.requirement.class'), + $alias + ); + } + } + return $stateText; + } +} diff --git a/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php new file mode 100644 index 0000000..7e9044c --- /dev/null +++ b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Module\Setup\Requirement; + +class ConfigDirectoryRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === null) { + return mt('setup', 'Read- and writable configuration directory'); + } + + return $title; + } + + protected function evaluate() + { + $path = $this->getCondition(); + if (file_exists($path)) { + $readable = is_readable($path); + if ($readable && is_writable($path)) { + $this->setStateText(sprintf(mt('setup', 'The directory %s is read- and writable.'), $path)); + return true; + } else { + $this->setStateText(sprintf( + $readable + ? mt('setup', 'The directory %s is not writable.') + : mt('setup', 'The directory %s is not readable.'), + $path + )); + return false; + } + } else { + $this->setStateText(sprintf(mt('setup', 'The directory %s does not exist.'), $path)); + return false; + } + } +} diff --git a/modules/setup/library/Setup/Requirement/OSRequirement.php b/modules/setup/library/Setup/Requirement/OSRequirement.php new file mode 100644 index 0000000..760c97a --- /dev/null +++ b/modules/setup/library/Setup/Requirement/OSRequirement.php @@ -0,0 +1,27 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class OSRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === null) { + return sprintf(mt('setup', '%s Platform'), ucfirst($this->getCondition())); + } + + return $title; + } + + protected function evaluate() + { + $phpOS = Platform::getOperatingSystemName(); + $this->setStateText(sprintf(mt('setup', 'You are running PHP on a %s system.'), ucfirst($phpOS))); + return strtolower($phpOS) === strtolower($this->getCondition()); + } +} diff --git a/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php new file mode 100644 index 0000000..6c77af5 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php @@ -0,0 +1,22 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class PhpConfigRequirement extends Requirement +{ + protected function evaluate() + { + list($configDirective, $value) = $this->getCondition(); + $configValue = Platform::getPhpConfig($configDirective); + $this->setStateText( + $configValue + ? sprintf(mt('setup', 'The PHP config `%s\' is set to "%s".'), $configDirective, $configValue) + : sprintf(mt('setup', 'The PHP config `%s\' is not defined.'), $configDirective) + ); + return is_bool($value) ? $configValue == $value : $configValue === $value; + } +} diff --git a/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php new file mode 100644 index 0000000..f8ab129 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php @@ -0,0 +1,42 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class PhpModuleRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === $this->getAlias()) { + if ($title === null) { + $title = $this->getCondition(); + } + + return sprintf(mt('setup', 'PHP Module: %s'), $title); + } + + return $title; + } + + protected function evaluate() + { + $moduleName = $this->getCondition(); + if (Platform::extensionLoaded($moduleName)) { + $this->setStateText(sprintf( + mt('setup', 'The PHP module %s is available.'), + $this->getAlias() ?: $moduleName + )); + return true; + } else { + $this->setStateText(sprintf( + mt('setup', 'The PHP module %s is missing.'), + $this->getAlias() ?: $moduleName + )); + return false; + } + } +} diff --git a/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php new file mode 100644 index 0000000..b811ca8 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php @@ -0,0 +1,28 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement; + +class PhpVersionRequirement extends Requirement +{ + public function getTitle() + { + $title = parent::getTitle(); + if ($title === null) { + return mt('setup', 'PHP Version'); + } + + return $title; + } + + protected function evaluate() + { + $phpVersion = Platform::getPhpVersion(); + $this->setStateText(sprintf(mt('setup', 'You are running PHP version %s.'), $phpVersion)); + list($operator, $requiredVersion) = $this->getCondition(); + return version_compare($phpVersion, $requiredVersion, $operator); + } +} diff --git a/modules/setup/library/Setup/Requirement/SetRequirement.php b/modules/setup/library/Setup/Requirement/SetRequirement.php new file mode 100644 index 0000000..77cbaf0 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/SetRequirement.php @@ -0,0 +1,34 @@ +<?php +/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Module\Setup\Requirement; + +/** + * Add requirement field + * + * @package Icinga\Module\Setup\Requirement + */ +class SetRequirement extends Requirement +{ + protected function evaluate() + { + $condition = $this->getCondition(); + + if ($condition->getState()) { + $this->setStateText(sprintf( + mt('setup', '%s is available.'), + $this->getAlias() ?: $this->getTitle() + )); + return true; + } + + $this->setStateText(sprintf( + mt('setup', '%s is missing.'), + $this->getAlias() ?: $this->getTitle() + )); + + return false; + } +} diff --git a/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php new file mode 100644 index 0000000..bab587a --- /dev/null +++ b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Icinga; +use Icinga\Module\Setup\Requirement; + +class WebLibraryRequirement extends Requirement +{ + protected function evaluate() + { + list($name, $op, $version) = $this->getCondition(); + + $libs = Icinga::app()->getLibraries(); + if (! $libs->has($name)) { + $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias())); + return false; + } + + $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $libs->get($name)->getVersion())); + return $libs->has($name, $op . $version); + } +} diff --git a/modules/setup/library/Setup/Requirement/WebModuleRequirement.php b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php new file mode 100644 index 0000000..ad600e1 --- /dev/null +++ b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Module\Setup\Requirement; + +use Icinga\Application\Icinga; +use Icinga\Module\Setup\Requirement; + +class WebModuleRequirement extends Requirement +{ + protected function evaluate() + { + list($name, $op, $version) = $this->getCondition(); + + $mm = Icinga::app()->getModuleManager(); + if (! $mm->hasInstalled($name)) { + $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias())); + return false; + } + + $module = $mm->getModule($name, false); + + $moduleVersion = $module->getVersion(); + if ($moduleVersion[0] === 'v') { + $moduleVersion = substr($moduleVersion, 1); + } + + $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $moduleVersion)); + return version_compare($moduleVersion, $version, $op); + } +} diff --git a/modules/setup/library/Setup/RequirementSet.php b/modules/setup/library/Setup/RequirementSet.php new file mode 100644 index 0000000..672fad4 --- /dev/null +++ b/modules/setup/library/Setup/RequirementSet.php @@ -0,0 +1,335 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use LogicException; +use RecursiveIterator; +use Traversable; + +/** + * Container to store and handle requirements + */ +class RequirementSet implements RecursiveIterator +{ + /** + * Mode AND (all requirements must be met) + */ + const MODE_AND = 0; + + /** + * Mode OR (at least one requirement must be met) + */ + const MODE_OR = 1; + + /** + * Whether all requirements meet their condition + * + * @var bool + */ + protected $state; + + /** + * Whether this set is optional + * + * @var bool + */ + protected $optional; + + /** + * The mode by which the requirements are evaluated + * + * @var string + */ + protected $mode; + + /** + * The registered requirements + * + * @var array + */ + protected $requirements; + + /** + * The raw state of this set's requirements + * + * @var bool + */ + private $forcedState; + + /** + * Initialize a new set of requirements + * + * @param bool $optional Whether this set is optional + * @param int $mode The mode by which to evaluate this set + */ + public function __construct($optional = false, $mode = null) + { + $this->optional = $optional; + $this->requirements = array(); + $this->setMode($mode ?: static::MODE_AND); + } + + /** + * Set the state of this set + * + * @param bool $state + * + * @return RequirementSet + */ + public function setState($state) + { + $this->state = (bool) $state; + return $this; + } + + /** + * Return the state of this set + * + * Alias for RequirementSet::fulfilled(true). + * + * @return bool + */ + public function getState() + { + return $this->fulfilled(true); + } + + /** + * Set whether this set of requirements should be optional + * + * @param bool $state + * + * @return RequirementSet + */ + public function setOptional($state = true) + { + $this->optional = (bool) $state; + return $this; + } + + /** + * Return whether this set of requirements is optional + * + * @return bool + */ + public function isOptional() + { + return $this->optional; + } + + /** + * Set the mode by which to evaluate the requirements + * + * @param int $mode + * + * @return RequirementSet + * + * @throws LogicException In case the given mode is invalid + */ + public function setMode($mode) + { + if ($mode !== static::MODE_AND && $mode !== static::MODE_OR) { + throw new LogicException(sprintf('Invalid mode %u given.'), $mode); + } + + $this->mode = $mode; + return $this; + } + + /** + * Return the mode by which the requirements are evaluated + * + * @return int + */ + public function getMode() + { + return $this->mode; + } + + /** + * Register a requirement + * + * @param Requirement $requirement The requirement to add + * + * @return RequirementSet + */ + public function add(Requirement $requirement) + { + $merged = false; + foreach ($this->requirements as $knownRequirement) { + if ($knownRequirement instanceof Requirement && $requirement->equals($knownRequirement)) { + $knownRequirement->setOptional($requirement->isOptional()); + foreach ($requirement->getDescriptions() as $description) { + $knownRequirement->addDescription($description); + } + + $merged = true; + break; + } + } + + if (! $merged) { + $this->requirements[] = $requirement; + } + + return $this; + } + + /** + * Return all registered requirements + * + * @return array + */ + public function getAll() + { + return $this->requirements; + } + + /** + * Register the given set of requirements + * + * @param RequirementSet $set The set to register + * + * @return RequirementSet + */ + public function merge(RequirementSet $set) + { + if ($this->getMode() === $set->getMode() && $this->isOptional() === $set->isOptional()) { + foreach ($set->getAll() as $requirement) { + if ($requirement instanceof static) { + $this->merge($requirement); + } else { + $this->add($requirement); + } + } + } else { + $this->requirements[] = $set; + } + + return $this; + } + + /** + * Return whether all requirements can successfully be evaluated based on the current mode + * + * In case this is a optional set of requirements (and $force is false), true is returned immediately. + * + * @param bool $force Whether to ignore the optionality of a set or single requirement + * + * @return bool + */ + public function fulfilled($force = false) + { + $state = $this->isOptional(); + if (! $force && $state) { + return true; + } + + if (! $force && $this->state !== null) { + return $this->state; + } elseif ($force && $this->forcedState !== null) { + return $this->forcedState; + } + + $self = $this->requirements; + foreach ($self as $requirement) { + if ($requirement->getState()) { + $state = true; + if ($this->getMode() === static::MODE_OR) { + break; + } + } elseif ($force || !$requirement->isOptional()) { + $state = false; + if ($this->getMode() === static::MODE_AND) { + break; + } + } + } + + if ($force) { + return $this->forcedState = $state; + } + + return $this->state = $state; + } + + /** + * Return whether the current element represents a nested set of requirements + * + * @return bool + */ + public function hasChildren(): bool + { + $current = $this->current(); + return $current instanceof static; + } + + /** + * Return a iterator for the current nested set of requirements + * + * @return ?RecursiveIterator + */ + public function getChildren(): ?RecursiveIterator + { + return $this->current(); + } + + /** + * Rewind the iterator to its first element + */ + public function rewind(): void + { + reset($this->requirements); + } + + /** + * Return whether the current iterator position is valid + * + * @return bool + */ + public function valid(): bool + { + return key($this->requirements) !== null; + } + + /** + * Return the current element in the iteration + * + * @return Requirement|RequirementSet + */ + #[\ReturnTypeWillChange] + public function current() + { + return current($this->requirements); + } + + /** + * Return the position of the current element in the iteration + * + * @return int + */ + public function key(): int + { + return key($this->requirements); + } + + /** + * Advance the iterator to the next element + */ + public function next(): void + { + next($this->requirements); + } + + /** + * Return this set of requirements rendered as HTML + * + * @return string + */ + public function __toString() + { + $renderer = new RequirementsRenderer($this); + return (string) $renderer; + } +} diff --git a/modules/setup/library/Setup/RequirementsRenderer.php b/modules/setup/library/Setup/RequirementsRenderer.php new file mode 100644 index 0000000..cc9392a --- /dev/null +++ b/modules/setup/library/Setup/RequirementsRenderer.php @@ -0,0 +1,64 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use RecursiveIteratorIterator; + +class RequirementsRenderer extends RecursiveIteratorIterator +{ + public function beginIteration(): void + { + $this->tags[] = '<ul class="requirements">'; + } + + public function endIteration(): void + { + $this->tags[] = '</ul>'; + } + + public function beginChildren(): void + { + $this->tags[] = '<li>'; + $currentSet = $this->getSubIterator(); + $state = $currentSet->getState() ? 'fulfilled' : ($currentSet->isOptional() ? 'not-available' : 'missing'); + $this->tags[] = '<ul class="set-state ' . $state . '">'; + } + + public function endChildren(): void + { + $this->tags[] = '</ul>'; + $this->tags[] = '</li>'; + } + + public function render() + { + foreach ($this as $requirement) { + $this->tags[] = '<li class="clearfix">'; + $this->tags[] = '<div class="title"><h2>' . $requirement->getTitle() . '</h2></div>'; + $this->tags[] = '<div class="description">'; + $descriptions = $requirement->getDescriptions(); + if (count($descriptions) > 1) { + $this->tags[] = '<ul>'; + foreach ($descriptions as $d) { + $this->tags[] = '<li>' . $d . '</li>'; + } + $this->tags[] = '</ul>'; + } elseif (! empty($descriptions)) { + $this->tags[] = $descriptions[0]; + } + $this->tags[] = '</div>'; + $this->tags[] = '<div class="state ' . ($requirement->getState() ? 'fulfilled' : ( + $requirement->isOptional() ? 'not-available' : 'missing' + )) . '">' . $requirement->getStateText() . '</div>'; + $this->tags[] = '</li>'; + } + + return implode("\n", $this->tags); + } + + public function __toString() + { + return $this->render(); + } +} diff --git a/modules/setup/library/Setup/Setup.php b/modules/setup/library/Setup/Setup.php new file mode 100644 index 0000000..7b0baed --- /dev/null +++ b/modules/setup/library/Setup/Setup.php @@ -0,0 +1,99 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use ArrayIterator; +use IteratorAggregate; +use Icinga\Module\Setup\Exception\SetupException; +use Traversable; + +/** + * Container for multiple configuration steps + */ +class Setup implements IteratorAggregate +{ + protected $steps; + + protected $state; + + public function __construct() + { + $this->steps = array(); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->getSteps()); + } + + public function addStep(Step $step) + { + $this->steps[] = $step; + } + + public function addSteps(array $steps) + { + foreach ($steps as $step) { + $this->addStep($step); + } + } + + public function getSteps() + { + return $this->steps; + } + + /** + * Run the configuration and return whether it succeeded + * + * @return bool + */ + public function run() + { + $this->state = true; + + try { + foreach ($this->steps as $step) { + $this->state &= $step->apply(); + } + } catch (SetupException $_) { + $this->state = false; + } + + return $this->state; + } + + /** + * Return a summary of all actions designated to run + * + * @return array An array of HTML strings + */ + public function getSummary() + { + $summaries = array(); + foreach ($this->steps as $step) { + $summaries[] = $step->getSummary(); + } + + return $summaries; + } + + /** + * Return a report of all actions that were run + * + * @return array An array of arrays of strings + */ + public function getReport() + { + $reports = array(); + foreach ($this->steps as $step) { + $report = $step->getReport(); + if (! empty($report)) { + $reports[] = $report; + } + } + + return $reports; + } +} diff --git a/modules/setup/library/Setup/SetupWizard.php b/modules/setup/library/Setup/SetupWizard.php new file mode 100644 index 0000000..c7ad0c3 --- /dev/null +++ b/modules/setup/library/Setup/SetupWizard.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +/** + * Interface for wizards providing a setup and requirements + */ +interface SetupWizard +{ + /** + * Return the setup for this wizard + * + * @return Setup + */ + public function getSetup(); + + /** + * Return the requirements of this wizard + * + * @return RequirementSet + */ + public function getRequirements(); +} diff --git a/modules/setup/library/Setup/Step.php b/modules/setup/library/Setup/Step.php new file mode 100644 index 0000000..1d0797d --- /dev/null +++ b/modules/setup/library/Setup/Step.php @@ -0,0 +1,31 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +/** + * Class to implement functionality for a single setup step + */ +abstract class Step +{ + /** + * Apply this step's configuration changes + * + * @return bool + */ + abstract public function apply(); + + /** + * Return a HTML representation of this step's configuration changes supposed to be made + * + * @return string + */ + abstract public function getSummary(); + + /** + * Return a textual summary of all configuration changes made + * + * @return array + */ + abstract public function getReport(); +} diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php new file mode 100644 index 0000000..3c6c64a --- /dev/null +++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php @@ -0,0 +1,238 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Config; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\IcingaException; +use Icinga\Authentication\User\DbUserBackend; +use Icinga\Module\Setup\Step; + +class AuthenticationStep extends Step +{ + protected $data; + + protected $dbError; + + protected $authIniError; + + protected $permIniError; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $success = $this->createAuthenticationIni(); + if (isset($this->data['adminAccountData']['resourceConfig'])) { + $success &= $this->createAccount(); + } + + $success &= $this->createRolesIni(); + return $success; + } + + protected function createAuthenticationIni() + { + $config = array(); + $backendConfig = $this->data['backendConfig']; + $backendName = $backendConfig['name']; + unset($backendConfig['name']); + $config[$backendName] = $backendConfig; + if (isset($this->data['resourceName'])) { + $config[$backendName]['resource'] = $this->data['resourceName']; + } + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('authentication.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->authIniError = $e; + return false; + } + + $this->authIniError = false; + return true; + } + + protected function createRolesIni() + { + if (isset($this->data['adminAccountData']['username'])) { + $config = array( + 'users' => $this->data['adminAccountData']['username'], + 'permissions' => '*' + ); + + if ($this->data['backendConfig']['backend'] === 'db') { + $config['groups'] = mt('setup', 'Administrators', 'setup.role.name'); + } + } else { // isset($this->data['adminAccountData']['groupname']) + $config = array( + 'groups' => $this->data['adminAccountData']['groupname'], + 'permissions' => '*' + ); + } + + try { + Config::fromArray(array(mt('setup', 'Administrators', 'setup.role.name') => $config)) + ->setConfigFile(Config::resolvePath('roles.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->permIniError = $e; + return false; + } + + $this->permIniError = false; + return true; + } + + protected function createAccount() + { + try { + $backend = new DbUserBackend( + ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig'])) + ); + + if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) { + $backend->insert('user', array( + 'user_name' => $this->data['adminAccountData']['username'], + 'password' => $this->data['adminAccountData']['password'], + 'is_active' => true + )); + $this->dbError = false; + } + } catch (Exception $e) { + $this->dbError = $e; + return false; + } + + return true; + } + + public function getSummary() + { + $pageTitle = '<h2>' . mt('setup', 'Authentication', 'setup.page.title') . '</h2>'; + $backendTitle = '<h3>' . mt('setup', 'Authentication Backend', 'setup.page.title') . '</h3>'; + $adminTitle = '<h3>' . mt('setup', 'Administration', 'setup.page.title') . '</h3>'; + + $authType = $this->data['backendConfig']['backend']; + $backendDesc = '<p>' . sprintf( + mt('setup', 'Users will authenticate using %s.', 'setup.summary.auth'), + $authType === 'db' ? mt('setup', 'a database', 'setup.summary.auth.type') : ( + $authType === 'ldap' || $authType === 'msldap' ? 'LDAP' : ( + mt('setup', 'webserver authentication', 'setup.summary.auth.type') + ) + ) + ) . '</p>'; + + $backendHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Backend Name') . '</strong></td>' + . '<td>' . $this->data['backendConfig']['name'] . '</td>' + . '</tr>' + . ($authType === 'ldap' || $authType === 'msldap' ? ( + '<tr>' + . '<td><strong>' . mt('setup', 'User Object Class') . '</strong></td>' + . '<td>' . ($authType === 'msldap' ? 'user' : $this->data['backendConfig']['user_class']) . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>' + . '<td>' . (trim($this->data['backendConfig']['filter']) ?: t('None', 'auth.ldap.filter')) . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'User Name Attribute') . '</strong></td>' + . '<td>' . ($authType === 'msldap' + ? 'sAMAccountName' + : $this->data['backendConfig']['user_name_attribute']) . '</td>' + . '</tr>' + ) : ($authType === 'external' ? ( + '<tr>' + . '<td><strong>' . t('Filter Pattern') . '</strong></td>' + . '<td>' . $this->data['backendConfig']['strip_username_regexp'] . '</td>' + . '</tr>' + ) : '')) + . '</tbody>' + . '</table>'; + + if (isset($this->data['adminAccountData']['username'])) { + $adminHtml = '<p>' . (isset($this->data['adminAccountData']['resourceConfig']) ? sprintf( + mt('setup', 'Administrative rights will initially be granted to a new account called "%s".'), + $this->data['adminAccountData']['username'] + ) : sprintf( + mt('setup', 'Administrative rights will initially be granted to an existing account called "%s".'), + $this->data['adminAccountData']['username'] + )) . '</p>'; + } else { // isset($this->data['adminAccountData']['groupname']) + $adminHtml = '<p>' . sprintf( + mt('setup', 'Administrative rights will initially be granted to members of the user group "%s".'), + $this->data['adminAccountData']['groupname'] + ) . '</p>'; + } + + return $pageTitle . '<div class="topic">' . $backendDesc . $backendTitle . $backendHtml . '</div>' + . '<div class="topic">' . $adminTitle . $adminHtml . '</div>'; + } + + public function getReport() + { + $report = array(); + + if ($this->authIniError === false) { + $report[] = sprintf( + mt('setup', 'Authentication configuration has been successfully written to: %s'), + Config::resolvePath('authentication.ini') + ); + } elseif ($this->authIniError !== null) { + $report[] = sprintf( + mt('setup', 'Authentication configuration could not be written to: %s. An error occured:'), + Config::resolvePath('authentication.ini') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->authIniError)); + } + + if ($this->dbError === false) { + $report[] = sprintf( + mt('setup', 'Account "%s" has been successfully created.'), + $this->data['adminAccountData']['username'] + ); + } elseif ($this->dbError !== null) { + $report[] = sprintf( + mt('setup', 'Unable to create account "%s". An error occured:'), + $this->data['adminAccountData']['username'] + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->dbError)); + } + + if ($this->permIniError === false) { + $report[] = isset($this->data['adminAccountData']['username']) ? sprintf( + mt('setup', 'Account "%s" has been successfully defined as initial administrator.'), + $this->data['adminAccountData']['username'] + ) : sprintf( + mt('setup', 'The members of the user group "%s" were successfully defined as initial administrators.'), + $this->data['adminAccountData']['groupname'] + ); + } elseif ($this->permIniError !== null) { + $report[] = isset($this->data['adminAccountData']['username']) ? sprintf( + mt('setup', 'Unable to define account "%s" as initial administrator. An error occured:'), + $this->data['adminAccountData']['username'] + ) : sprintf( + mt( + 'setup', + 'Unable to define the members of the user group "%s" as initial administrators. An error occured:' + ), + $this->data['adminAccountData']['groupname'] + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->permIniError)); + } + + return $report; + } +} diff --git a/modules/setup/library/Setup/Steps/DatabaseStep.php b/modules/setup/library/Setup/Steps/DatabaseStep.php new file mode 100644 index 0000000..32b2d15 --- /dev/null +++ b/modules/setup/library/Setup/Steps/DatabaseStep.php @@ -0,0 +1,266 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use PDOException; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; +use Icinga\Module\Setup\Utils\DbTool; +use Icinga\Module\Setup\Exception\SetupException; + +class DatabaseStep extends Step +{ + protected $data; + + protected $error; + + protected $messages; + + public function __construct(array $data) + { + $this->data = $data; + $this->messages = array(); + } + + public function apply() + { + $resourceConfig = $this->data['resourceConfig']; + if (isset($this->data['adminName'])) { + $resourceConfig['username'] = $this->data['adminName']; + if (isset($this->data['adminPassword'])) { + $resourceConfig['password'] = $this->data['adminPassword']; + } + } + + $db = new DbTool($resourceConfig); + + try { + if ($resourceConfig['db'] === 'mysql') { + $this->setupMysqlDatabase($db); + } elseif ($resourceConfig['db'] === 'pgsql') { + $this->setupPgsqlDatabase($db); + } + } catch (Exception $e) { + $this->error = $e; + throw new SetupException(); + } + + $this->error = false; + return true; + } + + protected function setupMysqlDatabase(DbTool $db) + { + try { + $db->connectToDb(); + $this->log( + mt('setup', 'Successfully connected to existing database "%s"...'), + $this->data['resourceConfig']['dbname'] + ); + } catch (PDOException $_) { + $db->connectToHost(); + $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']); + $db->exec('CREATE DATABASE ' . $db->quoteIdentifier($this->data['resourceConfig']['dbname'])); + $db->reconnect($this->data['resourceConfig']['dbname']); + } + + if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) { + $this->log(mt('setup', 'Database schema already exists...')); + } else { + $this->log(mt('setup', 'Creating database schema...')); + $db->import($this->data['schemaPath'] . '/mysql.schema.sql'); + } + + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']); + } else { + $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']); + $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']); + } + + $username = $this->data['resourceConfig']['username']; + if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) { + $this->log( + mt('setup', 'Required privileges were already granted to login "%s".'), + $this->data['resourceConfig']['username'] + ); + } else { + $this->log( + mt('setup', 'Granting required privileges to login "%s"...'), + $this->data['resourceConfig']['username'] + ); + $db->grantPrivileges( + $this->data['privileges'], + $this->data['tables'], + $this->data['resourceConfig']['username'] + ); + } + } + + protected function setupPgsqlDatabase(DbTool $db) + { + try { + $db->connectToDb(); + $this->log( + mt('setup', 'Successfully connected to existing database "%s"...'), + $this->data['resourceConfig']['dbname'] + ); + } catch (PDOException $_) { + $db->connectToHost(); + $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']); + $db->exec(sprintf( + "CREATE DATABASE %s WITH ENCODING 'UTF-8'", + $db->quoteIdentifier($this->data['resourceConfig']['dbname']) + )); + $db->reconnect($this->data['resourceConfig']['dbname']); + } + + if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) { + $this->log(mt('setup', 'Database schema already exists...')); + } else { + $this->log(mt('setup', 'Creating database schema...')); + $db->import($this->data['schemaPath'] . '/pgsql.schema.sql'); + } + + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']); + } else { + $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']); + $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']); + } + + $username = $this->data['resourceConfig']['username']; + if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) { + $this->log( + mt('setup', 'Required privileges were already granted to login "%s".'), + $this->data['resourceConfig']['username'] + ); + } else { + $this->log( + mt('setup', 'Granting required privileges to login "%s"...'), + $this->data['resourceConfig']['username'] + ); + $db->grantPrivileges( + $this->data['privileges'], + $this->data['tables'], + $this->data['resourceConfig']['username'] + ); + } + } + + public function getSummary() + { + $resourceConfig = $this->data['resourceConfig']; + if (isset($this->data['adminName'])) { + $resourceConfig['username'] = $this->data['adminName']; + if (isset($this->data['adminPassword'])) { + $resourceConfig['password'] = $this->data['adminPassword']; + } + } + + $db = new DbTool($resourceConfig); + + try { + $db->connectToDb(); + if (array_search(reset($this->data['tables']), $db->listTables(), true) === false) { + if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to setup the missing schema required by Icinga' + . ' Web 2 in database "%s" and to grant access to it to a new login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } else { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to setup the missing' + . ' schema required by Icinga Web 2 in database "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'] + ); + } + } else { + $message = sprintf( + mt('setup', 'The database "%s" already seems to be fully set up. No action required.'), + $resourceConfig['dbname'] + ); + } + } catch (PDOException $_) { + try { + $db->connectToHost(); + if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) { + if ($db->hasLogin($this->data['resourceConfig']['username'])) { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to create the missing database' + . ' "%s" with the schema required by Icinga Web 2 and to grant' + . ' access to it to an existing login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } else { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to create the missing database' + . ' "%s" with the schema required by Icinga Web 2 and to grant' + . ' access to it to a new login called "%s".' + ), + $resourceConfig['username'], + $resourceConfig['dbname'], + $this->data['resourceConfig']['username'] + ); + } + } else { + $message = sprintf( + mt( + 'setup', + 'The database user "%s" will be used to create the missing' + . ' database "%s" with the schema required by Icinga Web 2.' + ), + $resourceConfig['username'], + $resourceConfig['dbname'] + ); + } + } catch (Exception $_) { + $message = mt( + 'setup', + 'No connection to database host possible. You\'ll need to setup the' + . ' database with the schema required by Icinga Web 2 manually.' + ); + } + } + + return '<h2>' . mt('setup', 'Database Setup', 'setup.page.title') . '</h2><p>' . $message . '</p>'; + } + + public function getReport() + { + if ($this->error === false) { + $report = $this->messages; + $report[] = mt('setup', 'The database has been fully set up!'); + return $report; + } elseif ($this->error !== null) { + $report = $this->messages; + $report[] = mt('setup', 'Failed to fully setup the database. An error occured:'); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)); + return $report; + } + } + + protected function log() + { + $this->messages[] = call_user_func_array('sprintf', func_get_args()); + } +} diff --git a/modules/setup/library/Setup/Steps/GeneralConfigStep.php b/modules/setup/library/Setup/Steps/GeneralConfigStep.php new file mode 100644 index 0000000..2c928f6 --- /dev/null +++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class GeneralConfigStep extends Step +{ + protected $data; + + protected $error; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $config = array(); + foreach ($this->data['generalConfig'] as $sectionAndPropertyName => $value) { + list($section, $property) = explode('_', $sectionAndPropertyName, 2); + $config[$section][$property] = $value; + } + + $config['global']['config_resource'] = $this->data['resourceName']; + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('config.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '<h2>' . mt('setup', 'Application Configuration', 'setup.page.title') . '</h2>'; + $generalTitle = '<h3>' . t('General', 'app.config') . '</h3>'; + $loggingTitle = '<h3>' . t('Logging', 'app.config') . '</h3>'; + + $generalHtml = '' + . '<ul>' + . '<li>' . ($this->data['generalConfig']['global_show_stacktraces'] + ? t('An exception\'s stacktrace is shown to every user by default.') + : t('An exception\'s stacktrace is hidden from every user by default.') + ) . '</li>' + . '<li>' . t('Preferences will be stored using a database.') . '</li>' + . '</ul>'; + + $type = $this->data['generalConfig']['logging_log']; + if ($type === 'none') { + $loggingHtml = '<p>' . mt('setup', 'Logging will be disabled.') . '</p>'; + } else { + $level = $this->data['generalConfig']['logging_level']; + + switch ($type) { + case 'php': + $typeDescription = t('Webserver Log', 'app.config.logging.type'); + $typeSpecificHtml = ''; + break; + + case 'syslog': + $typeDescription = 'Syslog'; + $typeSpecificHtml = '<td><strong>' . t('Application Prefix') . '</strong></td>' + . '<td>' . $this->data['generalConfig']['logging_application'] . '</td>'; + break; + + case 'file': + $typeDescription = t('File', 'app.config.logging.type'); + $typeSpecificHtml = '<td><strong>' . t('Filepath') . '</strong></td>' + . '<td>' . $this->data['generalConfig']['logging_file'] . '</td>'; + break; + } + + $loggingHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Type', 'app.config.logging') . '</strong></td>' + . '<td>' . $typeDescription . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Level', 'app.config.logging') . '</strong></td>' + . '<td>' . ($level === Logger::$levels[Logger::ERROR] ? t('Error', 'app.config.logging.level') : ( + $level === Logger::$levels[Logger::WARNING] ? t('Warning', 'app.config.logging.level') : ( + $level === Logger::$levels[Logger::INFO] ? t('Information', 'app.config.logging.level') : ( + t('Debug', 'app.config.logging.level') + ) + ) + )) . '</td>' + . '</tr>' + . '<tr>' + . $typeSpecificHtml + . '</tr>' + . '</tbody>' + . '</table>'; + } + + return $pageTitle . '<div class="topic">' . $generalTitle . $generalHtml . '</div>' + . '<div class="topic">' . $loggingTitle . $loggingHtml . '</div>'; + } + + public function getReport() + { + if ($this->error === false) { + return array(sprintf( + mt('setup', 'General configuration has been successfully written to: %s'), + Config::resolvePath('config.ini') + )); + } elseif ($this->error !== null) { + return array( + sprintf( + mt('setup', 'General configuration could not be written to: %s. An error occured:'), + Config::resolvePath('config.ini') + ), + sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) + ); + } + } +} diff --git a/modules/setup/library/Setup/Steps/ResourceStep.php b/modules/setup/library/Setup/Steps/ResourceStep.php new file mode 100644 index 0000000..d9daf3b --- /dev/null +++ b/modules/setup/library/Setup/Steps/ResourceStep.php @@ -0,0 +1,199 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Config; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class ResourceStep extends Step +{ + protected $data; + + protected $error; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $resourceConfig = array(); + if (isset($this->data['dbResourceConfig'])) { + $dbConfig = $this->data['dbResourceConfig']; + $resourceName = $dbConfig['name']; + unset($dbConfig['name']); + $resourceConfig[$resourceName] = $dbConfig; + } + + if (isset($this->data['ldapResourceConfig'])) { + $ldapConfig = $this->data['ldapResourceConfig']; + $resourceName = $ldapConfig['name']; + unset($ldapConfig['name']); + $resourceConfig[$resourceName] = $ldapConfig; + } + + try { + Config::fromArray($resourceConfig) + ->setConfigFile(Config::resolvePath('resources.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + if (isset($this->data['dbResourceConfig']) && isset($this->data['ldapResourceConfig'])) { + $pageTitle = '<h2>' . mt('setup', 'Resources', 'setup.page.title') . '</h2>'; + } else { + $pageTitle = '<h2>' . mt('setup', 'Resource', 'setup.page.title') . '</h2>'; + } + + if (isset($this->data['dbResourceConfig'])) { + $dbTitle = '<h3>' . mt('setup', 'Database', 'setup.page.title') . '</h3>'; + $dbHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Resource Name') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Database Type') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['db'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Host') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['host'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Port') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Database Name') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['dbname'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Username') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['username'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Password') . '</strong></td>' + . '<td>' . str_repeat('*', strlen($this->data['dbResourceConfig']['password'])) . '</td>' + . '</tr>'; + + if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && isset($this->data['resourceConfig']['ssl_do_not_verify_server_cert']) + && $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] + ) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('SSL Do Not Verify Server Certificate') . '</strong></td>' + . '<td>' . $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_key']) && $this->data['dbResourceConfig']['ssl_key']) { + $dbHtml .= '' + .'<tr>' + . '<td><strong>' . t('SSL Key') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_key'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_cert']) && $this->data['dbResourceConfig']['ssl_cert']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('SSL Cert') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_cert'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_ca']) && $this->data['dbResourceConfig']['ssl_ca']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('CA') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_ca'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_capath']) && $this->data['dbResourceConfig']['ssl_capath']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('CA Path') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_capath'] . '</td>' + . '</tr>'; + } + if (isset($this->data['dbResourceConfig']['ssl_cipher']) && $this->data['dbResourceConfig']['ssl_cipher']) { + $dbHtml .= '' + . '<tr>' + . '<td><strong>' . t('Cipher') . '</strong></td>' + . '<td>' . $this->data['dbResourceConfig']['ssl_cipher'] . '</td>' + . '</tr>'; + } + + $dbHtml .= '' + . '</tbody>' + . '</table>'; + } + + if (isset($this->data['ldapResourceConfig'])) { + $ldapTitle = '<h3>LDAP</h3>'; + $ldapHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Resource Name') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Host') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['hostname'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Port') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['port'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Root DN') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['root_dn'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Bind DN') . '</strong></td>' + . '<td>' . $this->data['ldapResourceConfig']['bind_dn'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . t('Bind Password') . '</strong></td>' + . '<td>' . str_repeat('*', strlen($this->data['ldapResourceConfig']['bind_pw'])) . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + } + + return $pageTitle . (isset($dbTitle) ? '<div class="topic">' . $dbTitle . $dbHtml . '</div>' : '') + . (isset($ldapTitle) ? '<div class="topic">' . $ldapTitle . $ldapHtml . '</div>' : ''); + } + + public function getReport() + { + if ($this->error === false) { + return array(sprintf( + mt('setup', 'Resource configuration has been successfully written to: %s'), + Config::resolvePath('resources.ini') + )); + } elseif ($this->error !== null) { + return array( + sprintf( + mt('setup', 'Resource configuration could not be written to: %s. An error occured:'), + Config::resolvePath('resources.ini') + ), + sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) + ); + } + } +} diff --git a/modules/setup/library/Setup/Steps/UserGroupStep.php b/modules/setup/library/Setup/Steps/UserGroupStep.php new file mode 100644 index 0000000..4aab676 --- /dev/null +++ b/modules/setup/library/Setup/Steps/UserGroupStep.php @@ -0,0 +1,213 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Steps; + +use Exception; +use Icinga\Application\Config; +use Icinga\Authentication\UserGroup\DbUserGroupBackend; +use Icinga\Data\ConfigObject; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class UserGroupStep extends Step +{ + protected $data; + + protected $groupError; + + protected $memberError; + + protected $groupIniError; + + public function __construct(array $data) + { + $this->data = $data; + } + + public function apply() + { + $success = $this->createGroupsIni(); + if (isset($this->data['resourceConfig'])) { + $success &= $this->createUserGroup(); + if ($success) { + $success &= $this->createMembership(); + } + } + + return $success; + } + + protected function createGroupsIni() + { + $config = array(); + if (isset($this->data['groupConfig'])) { + $backendConfig = $this->data['groupConfig']; + $backendName = $backendConfig['name']; + unset($backendConfig['name']); + $config[$backendName] = $backendConfig; + } else { + $backendConfig = array( + 'backend' => $this->data['backendConfig']['backend'], // "db" or "msldap" + 'resource' => $this->data['resourceName'] + ); + + if ($backendConfig['backend'] === 'msldap') { + $backendConfig['user_backend'] = $this->data['backendConfig']['name']; + } + + $config[$this->data['backendConfig']['name']] = $backendConfig; + } + + try { + Config::fromArray($config) + ->setConfigFile(Config::resolvePath('groups.ini')) + ->saveIni(); + } catch (Exception $e) { + $this->groupIniError = $e; + return false; + } + + $this->groupIniError = false; + return true; + } + + protected function createUserGroup() + { + try { + $backend = new DbUserGroupBackend( + ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig'])) + ); + + $groupName = mt('setup', 'Administrators', 'setup.role.name'); + if ($backend->select()->where('group_name', $groupName)->count() === 0) { + $backend->insert('group', array( + 'group_name' => $groupName + )); + $this->groupError = false; + } + } catch (Exception $e) { + $this->groupError = $e; + return false; + } + + return true; + } + + protected function createMembership() + { + try { + $backend = new DbUserGroupBackend( + ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig'])) + ); + + $groupName = mt('setup', 'Administrators', 'setup.role.name'); + $userName = $this->data['username']; + if ($backend + ->select() + ->from('group_membership') + ->where('group_name', $groupName) + ->where('user_name', $userName) + ->count() === 0 + ) { + $backend->insert('group_membership', array( + 'group_name' => $groupName, + 'user_name' => $userName + )); + $this->memberError = false; + } + } catch (Exception $e) { + $this->memberError = $e; + return false; + } + + return true; + } + + public function getSummary() + { + if (! isset($this->data['groupConfig'])) { + return; // It's not necessary to show the user something he didn't configure.. + } + + $pageTitle = '<h2>' . mt('setup', 'User Groups', 'setup.page.title') . '</h2>'; + $backendTitle = '<h3>' . mt('setup', 'User Group Backend', 'setup.page.title') . '</h3>'; + + $backendHtml = '' + . '<table>' + . '<tbody>' + . '<tr>' + . '<td><strong>' . t('Backend Name') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['name'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Group Object Class') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['group_class'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>' + . '<td>' . (trim($this->data['groupConfig']['group_filter']) ?: t('None', 'auth.ldap.filter')) . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Group Name Attribute') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['group_name_attribute'] . '</td>' + . '</tr>' + . '<tr>' + . '<td><strong>' . mt('setup', 'Group Member Attribute') . '</strong></td>' + . '<td>' . $this->data['groupConfig']['group_member_attribute'] . '</td>' + . '</tr>' + . '</tbody>' + . '</table>'; + + return $pageTitle . '<div class="topic">' . $backendTitle . $backendHtml . '</div>'; + } + + public function getReport() + { + $report = array(); + + if ($this->groupIniError === false) { + $report[] = sprintf( + mt('setup', 'User Group Backend configuration has been successfully written to: %s'), + Config::resolvePath('groups.ini') + ); + } elseif ($this->groupIniError !== null) { + $report[] = sprintf( + mt('setup', 'User Group Backend configuration could not be written to: %s. An error occured:'), + Config::resolvePath('groups.ini') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupIniError)); + } + + if ($this->groupError === false) { + $report[] = sprintf( + mt('setup', 'User Group "%s" has been successfully created.'), + mt('setup', 'Administrators', 'setup.role.name') + ); + } elseif ($this->groupError !== null) { + $report[] = sprintf( + mt('setup', 'Unable to create user group "%s". An error occured:'), + mt('setup', 'Administrators', 'setup.role.name') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupError)); + } + + if ($this->memberError === false) { + $report[] = sprintf( + mt('setup', 'Account "%s" has been successfully added as member to user group "%s".'), + $this->data['username'], + mt('setup', 'Administrators', 'setup.role.name') + ); + } elseif ($this->memberError !== null) { + $report[] = sprintf( + mt('setup', 'Unable to add account "%s" as member to user group "%s". An error occured:'), + $this->data['username'], + mt('setup', 'Administrators', 'setup.role.name') + ); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->memberError)); + } + + return $report; + } +} diff --git a/modules/setup/library/Setup/Utils/DbTool.php b/modules/setup/library/Setup/Utils/DbTool.php new file mode 100644 index 0000000..5cf203e --- /dev/null +++ b/modules/setup/library/Setup/Utils/DbTool.php @@ -0,0 +1,943 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Utils; + +use PDO; +use PDOException; +use LogicException; +use Zend_Db_Adapter_Pdo_Mysql; +use Zend_Db_Adapter_Pdo_Pgsql; +use Icinga\Util\File; +use Icinga\Exception\ConfigurationError; + +/** + * Utility class to ease working with databases when setting up Icinga Web 2 or one of its modules + */ +class DbTool +{ + /** + * The PDO database connection + * + * @var PDO + */ + protected $pdoConn; + + /** + * The Zend database adapter + * + * @var Zend_Db_Adapter_Pdo_Abstract + */ + protected $zendConn; + + /** + * The resource configuration + * + * @var array + */ + protected $config; + + /** + * Whether we are connected to the database from the resource configuration + * + * @var bool + */ + protected $dbFromConfig = false; + + /** + * GRANT privilege level identifiers + */ + const GLOBAL_LEVEL = 1; + const PROCEDURE_LEVEL = 2; + const DATABASE_LEVEL = 4; + const TABLE_LEVEL = 8; + const COLUMN_LEVEL = 16; + const FUNCTION_LEVEL = 32; + + /** + * All MySQL GRANT privileges with their respective level identifiers + * + * @var array + */ + protected $mysqlGrantContexts = array( + 'ALL' => 31, + 'ALL PRIVILEGES' => 31, + 'ALTER' => 13, + 'ALTER ROUTINE' => 7, + 'CREATE' => 13, + 'CREATE ROUTINE' => 5, + 'CREATE TEMPORARY TABLES' => 5, + 'CREATE USER' => 1, + 'CREATE VIEW' => 13, + 'DELETE' => 13, + 'DROP' => 13, + 'EXECUTE' => 5, // MySQL reference states this also supports database level, 5.1.73 not though + 'FILE' => 1, + 'GRANT OPTION' => 15, + 'INDEX' => 13, + 'INSERT' => 29, + 'LOCK TABLES' => 5, + 'PROCESS' => 1, + 'REFERENCES' => 12, + 'RELOAD' => 1, + 'REPLICATION CLIENT' => 1, + 'REPLICATION SLAVE' => 1, + 'SELECT' => 29, + 'SHOW DATABASES' => 1, + 'SHOW VIEW' => 13, + 'SHUTDOWN' => 1, + 'SUPER' => 1, + 'UPDATE' => 29 + ); + + /** + * All PostgreSQL GRANT privileges with their respective level identifiers + * + * @var array + */ + protected $pgsqlGrantContexts = array( + 'ALL' => 63, + 'ALL PRIVILEGES' => 63, + 'SELECT' => 24, + 'INSERT' => 24, + 'UPDATE' => 24, + 'DELETE' => 8, + 'TRUNCATE' => 8, + 'REFERENCES' => 24, + 'TRIGGER' => 8, + 'CREATE' => 12, + 'CONNECT' => 4, + 'TEMPORARY' => 4, + 'TEMP' => 4, + 'EXECUTE' => 32, + 'USAGE' => 33, + 'CREATEROLE' => 1 + ); + + /** + * Create a new DbTool + * + * @param array $config The resource configuration to use + */ + public function __construct(array $config) + { + if (! isset($config['port'])) { + // TODO: This is not quite correct, but works as it previously did. Previously empty values were not + // transformed no NULL (now they are) so if the port is now null, it's been the empty string. + $config['port'] = ''; + } + + $this->config = $config; + } + + /** + * Connect to the server + * + * @return $this + */ + public function connectToHost() + { + $this->assertHostAccess(); + + if ($this->config['db'] == 'pgsql') { + // PostgreSQL requires us to specify a database on each connection and will use + // the current user name as default database in cases none is provided. If + // that database doesn't exist (which might be the case here) it will error. + // Therefore, we specify the maintenance database 'postgres' as database, which + // is most probably present and public. (http://stackoverflow.com/q/4483139) + $this->connect('postgres'); + } else { + $this->connect(); + } + + return $this; + } + + /** + * Connect to the database + * + * @return $this + */ + public function connectToDb() + { + $this->assertHostAccess(); + $this->assertDatabaseAccess(); + $this->connect($this->config['dbname']); + return $this; + } + + /** + * Assert that all configuration values exist that are required to connect to a server + * + * @throws ConfigurationError + */ + protected function assertHostAccess() + { + if (! isset($this->config['db'])) { + throw new ConfigurationError('Can\'t connect to database server of unknown type'); + } elseif (! isset($this->config['host'])) { + throw new ConfigurationError('Can\'t connect to database server without a hostname or address'); + } elseif (! isset($this->config['port'])) { + throw new ConfigurationError('Can\'t connect to database server without a port'); + } elseif (! isset($this->config['username'])) { + throw new ConfigurationError('Can\'t connect to database server without a username'); + } elseif (! isset($this->config['password'])) { + throw new ConfigurationError('Can\'t connect to database server without a password'); + } + } + + /** + * Assert that all configuration values exist that are required to connect to a database + * + * @throws ConfigurationError + */ + protected function assertDatabaseAccess() + { + if (! isset($this->config['dbname'])) { + throw new ConfigurationError('Can\'t connect to database without a valid database name'); + } + } + + /** + * Assert that a connection with a database has been established + * + * @throws LogicException + */ + protected function assertConnectedToDb() + { + if ($this->zendConn === null) { + throw new LogicException('Not connected to database'); + } + } + + /** + * Return whether a connection with the server has been established + * + * @return bool + */ + public function isConnected() + { + return $this->pdoConn !== null; + } + + /** + * Establish a connection with the database or just the server by omitting the database name + * + * @param string $dbname The name of the database to connect to + */ + public function connect($dbname = null) + { + $this->pdoConnect($dbname); + if ($dbname !== null) { + $this->zendConnect($dbname); + $this->dbFromConfig = $dbname === $this->config['dbname']; + } + } + + /** + * Reestablish a connection with the database or just the server by omitting the database name + * + * @param string $dbname The name of the database to connect to + */ + public function reconnect($dbname = null) + { + $this->pdoConn = null; + $this->zendConn = null; + $this->connect($dbname); + } + + /** + * Initialize Zend database adapter + * + * @param string $dbname The name of the database to connect with + * + * @throws ConfigurationError In case the resource type is not a supported PDO driver name + */ + private function zendConnect($dbname) + { + if ($this->zendConn !== null) { + return; + } + + $config = array( + 'dbname' => $dbname, + 'host' => $this->config['host'], + 'port' => $this->config['port'], + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ); + + if ($this->config['db'] === 'mysql') { + if (isset($this->config['use_ssl']) && $this->config['use_ssl']) { + $this->config['driver_options'] = array(); + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config['ssl_key']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key']; + } + if ($this->config['ssl_cert']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert']; + } + if ($this->config['ssl_ca']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca']; + } + if ($this->config['ssl_capath']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath']; + } + if ($this->config['ssl_cipher']) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher']; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config['ssl_do_not_verify_server_cert'] + ) { + $config['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + $this->zendConn = new Zend_Db_Adapter_Pdo_Mysql($config); + } elseif ($this->config['db'] === 'pgsql') { + $this->zendConn = new Zend_Db_Adapter_Pdo_Pgsql($config); + } else { + throw new ConfigurationError( + 'Failed to connect to database. Unsupported PDO driver "%s"', + $this->config['db'] + ); + } + + $this->zendConn->getConnection(); // Force connection attempt + } + + /** + * Initialize PDO connection + * + * @param string $dbname The name of the database to connect with + */ + private function pdoConnect($dbname) + { + if ($this->pdoConn !== null) { + return; + } + + $driverOptions = array( + PDO::ATTR_TIMEOUT => 1, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + + if ($this->config['db'] === 'mysql' + && isset($this->config['use_ssl']) + && $this->config['use_ssl'] + ) { + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config['ssl_key']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key']; + } + if ($this->config['ssl_cert']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert']; + } + if ($this->config['ssl_ca']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca']; + } + if ($this->config['ssl_capath']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath']; + } + if ($this->config['ssl_cipher']) { + $driverOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher']; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config['ssl_do_not_verify_server_cert'] + ) { + $driverOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + + $this->pdoConn = new PDO( + $this->buildDsn($this->config['db'], $dbname), + $this->config['username'], + $this->config['password'], + $driverOptions + ); + } + + /** + * Return a datasource name for the given database type and name + * + * @param string $dbtype + * @param string $dbname + * + * @return string + * + * @throws ConfigurationError In case the passed database type is not supported + */ + protected function buildDsn($dbtype, $dbname = null) + { + if ($dbtype === 'mysql') { + return 'mysql:host=' . $this->config['host'] . ';port=' . $this->config['port'] + . ($dbname !== null ? ';dbname=' . $dbname : ''); + } elseif ($dbtype === 'pgsql') { + return 'pgsql:host=' . $this->config['host'] . ';port=' . $this->config['port'] + . ($dbname !== null ? ';dbname=' . $dbname : ''); + } else { + throw new ConfigurationError( + 'Failed to build data source name. Unsupported PDO driver "%s"', + $dbtype + ); + } + } + + /** + * Try to connect to the server and throw an exception if this fails + * + * @throws PDOException In case an error occurs that does not indicate that authentication failed + */ + public function checkConnectivity() + { + try { + $this->connectToHost(); + } catch (PDOException $e) { + if ($this->config['db'] === 'mysql') { + $code = $e->getCode(); + /* + * 1040 .. Too many connections + * 1045 .. Access denied for user '%s'@'%s' (using password: %s) + * 1698 .. Access denied for user '%s'@'%s' + */ + if ($code !== 1040 && $code !== 1045 && $code !== 1698) { + throw $e; + } + } elseif ($this->config['db'] === 'pgsql') { + if (strpos($e->getMessage(), $this->config['username']) === false) { + throw $e; + } + } + } + } + + /** + * Return the given identifier escaped with backticks + * + * @param string $identifier The identifier to escape + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function quoteIdentifier($identifier) + { + if ($this->config['db'] === 'mysql') { + return '`' . str_replace('`', '``', $identifier) . '`'; + } elseif ($this->config['db'] === 'pgsql') { + return '"' . str_replace('"', '""', $identifier) . '"'; + } else { + throw new LogicException('Unable to quote identifier.'); + } + } + + /** + * Return the given table name with all wildcards being escaped + * + * @param string $tableName + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function escapeTableWildcards($tableName) + { + if ($this->config['db'] === 'mysql') { + return str_replace(array('_', '%'), array('\_', '\%'), $tableName); + } + + throw new LogicException('Unable to escape table wildcards.'); + } + + /** + * Return the given value escaped as string + * + * @param mixed $value The value to escape + * + * @return string + * + * @throws LogicException In case there is no behaviour implemented for the current PDO driver + */ + public function quote($value) + { + $quoted = $this->pdoConn->quote($value); + if ($quoted === false) { + throw new LogicException(sprintf('Unable to quote value: %s', $value)); + } + + return $quoted; + } + + /** + * Execute a SQL statement and return the affected row count + * + * Use $params to use a prepared statement. + * + * @param string $statement The statement to execute + * @param array $params The params to bind + * + * @return int + */ + public function exec($statement, $params = array()) + { + if (empty($params)) { + return $this->pdoConn->exec($statement); + } + + $stmt = $this->pdoConn->prepare($statement); + $stmt->execute($params); + return $stmt->rowCount(); + } + + /** + * Execute a SQL statement and return the result + * + * Use $params to use a prepared statement. + * + * @param string $statement The statement to execute + * @param array $params The params to bind + * + * @return mixed + */ + public function query($statement, $params = array()) + { + if ($this->zendConn !== null) { + return $this->zendConn->query($statement, $params); + } + + if (empty($params)) { + return $this->pdoConn->query($statement); + } + + $stmt = $this->pdoConn->prepare($statement); + $stmt->execute($params); + return $stmt; + } + + /** + * Return the version of the server currently connected to + * + * @return string|null + */ + public function getServerVersion() + { + if ($this->config['db'] === 'mysql') { + return $this->query('show variables like "version"')->fetchColumn(1) ?: null; + } elseif ($this->config['db'] === 'pgsql') { + return $this->query('show server_version')->fetchColumn() ?: null; + } else { + throw new LogicException( + sprintf('Unable to fetch the server\'s version. Unsupported PDO driver "%s"', $this->config['db']) + ); + } + } + + /** + * Import the given SQL file + * + * @param string $filepath The file to import + */ + public function import($filepath) + { + $file = new File($filepath); + $content = join(PHP_EOL, iterator_to_array($file)); // There is no fread() before PHP 5.5 :( + + foreach (preg_split('@;(?! \\\\)@', $content) as $statement) { + if (($statement = trim($statement)) !== '') { + $this->exec($statement); + } + } + } + + /** + * Return whether the given privileges were granted + * + * @param array $privileges An array of strings with the required privilege names + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name for which to check the privileges, + * if NULL the current login is used + * + * @return bool + */ + public function checkPrivileges(array $privileges, array $context = null, $username = null) + { + if ($this->config['db'] === 'mysql') { + return $this->checkMysqlPrivileges($privileges, false, $context, $username); + } elseif ($this->config['db'] === 'pgsql') { + return $this->checkPgsqlPrivileges($privileges, false, $context, $username); + } + } + + /** + * Return whether the given privileges are grantable to other users + * + * @param array $privileges The privileges that should be grantable + * + * @return bool + */ + public function isGrantable($privileges) + { + if ($this->config['db'] === 'mysql') { + return $this->checkMysqlPrivileges($privileges, true); + } elseif ($this->config['db'] === 'pgsql') { + return $this->checkPgsqlPrivileges($privileges, true); + } + } + + /** + * Grant all given privileges to the given user + * + * @param array $privileges The privilege names to grant + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The username to grant the privileges to + */ + public function grantPrivileges(array $privileges, array $context, $username) + { + if ($this->config['db'] === 'mysql') { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $quotedDbName = $this->quoteIdentifier($this->config['dbname']); + + $grant = 'GRANT %s'; + $on = ' ON %s.%s'; + $to = sprintf( + ' TO %s@%s', + $this->quoteIdentifier($username), + $this->quoteIdentifier($host) + ); + + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($tablePrivileges)) { + $tableGrant = sprintf($grant, join(',', $tablePrivileges)); + foreach ($context as $table) { + $this->exec($tableGrant . sprintf($on, $quotedDbName, $this->quoteIdentifier($table)) . $to); + } + } + + if (! empty($dbPrivileges)) { + $this->exec( + sprintf($grant, join(',', $dbPrivileges)) + . sprintf($on, $this->escapeTableWildcards($quotedDbName), '*') + . $to + ); + } + } elseif ($this->config['db'] === 'pgsql') { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($dbPrivileges)) { + $this->exec(sprintf( + 'GRANT %s ON DATABASE %s TO %s', + join(',', $dbPrivileges), + $this->config['dbname'], + $username + )); + } + + if (! empty($tablePrivileges)) { + foreach ($context as $table) { + $this->exec(sprintf( + 'GRANT %s ON TABLE %s TO %s', + join(',', $tablePrivileges), + $table, + $username + )); + } + } + } + } + + /** + * Return a list of all existing database tables + * + * @return array + */ + public function listTables() + { + $this->assertConnectedToDb(); + return $this->zendConn->listTables(); + } + + /** + * Return whether the given database login exists + * + * @param string $username The username to search + * + * @return bool + */ + public function hasLogin($username) + { + if ($this->config['db'] === 'mysql') { + $queryString = <<<EOD +SELECT 1 + FROM information_schema.user_privileges + WHERE grantee = REPLACE(CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'"), :current, :wanted) +EOD; + + $query = $this->query( + $queryString, + array( + ':current' => $this->config['username'], + ':wanted' => $username + ) + ); + return count($query->fetchAll()) > 0; + } elseif ($this->config['db'] === 'pgsql') { + $query = $this->query( + 'SELECT 1 FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1', + array(':ident' => $username) + ); + return count($query->fetchAll()) === 1; + } + } + + /** + * Add a new database login + * + * @param string $username The username of the new login + * @param string $password The password of the new login + */ + public function addLogin($username, $password) + { + if ($this->config['db'] === 'mysql') { + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $this->exec( + 'CREATE USER :user@:host IDENTIFIED BY :passw', + array(':user' => $username, ':host' => $host, ':passw' => $password) + ); + } elseif ($this->config['db'] === 'pgsql') { + $this->exec(sprintf( + 'CREATE USER %s WITH PASSWORD %s', + $this->quoteIdentifier($username), + $this->quote($password) + )); + } + } + + /** + * Check whether the current user has the given privileges + * + * @param array $privileges The privilege names + * @param bool $requireGrants Only return true when all privileges can be granted to others + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name to which the passed privileges need to be granted + * + * @return bool + */ + protected function checkMysqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $mysqlPrivileges = array_intersect($privileges, array_keys($this->mysqlGrantContexts)); + list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn()); + $grantee = "'" . ($username === null ? $this->config['username'] : $username) . "'@'" . $host . "'"; + + if (isset($this->config['dbname'])) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach ($mysqlPrivileges as $privilege) { + if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } + if ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + $dbPrivilegesGranted = true; + $tablePrivilegesGranted = true; + + if (! empty($dbPrivileges)) { + $queryString = 'SELECT COUNT(*) as matches' + . ' FROM information_schema.schema_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND privilege_type IN (%s)' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''); + + $dbAndTableQuery = $this->query( + sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbPrivileges))), + array(':grantee' => $grantee, ':dbname' => $this->escapeTableWildcards($this->config['dbname'])) + ); + $grantedDbAndTablePrivileges = (int) $dbAndTableQuery->fetchObject()->matches; + if ($grantedDbAndTablePrivileges === count($dbPrivileges)) { + $tableExclusivePrivileges = array_diff($tablePrivileges, $dbPrivileges); + if (! empty($tableExclusivePrivileges)) { + $tablePrivileges = $tableExclusivePrivileges; + $tablePrivilegesGranted = false; + } + } else { + $tablePrivilegesGranted = false; + $dbExclusivePrivileges = array_diff($dbPrivileges, $tablePrivileges); + if (! empty($dbExclusivePrivileges)) { + $dbExclusiveQuery = $this->query( + sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbExclusivePrivileges))), + array( + ':grantee' => $grantee, + ':dbname' => $this->escapeTableWildcards($this->config['dbname']) + ) + ); + $dbPrivilegesGranted = (int) $dbExclusiveQuery->fetchObject()->matches === count( + $dbExclusivePrivileges + ); + } + } + } + + if (! $tablePrivilegesGranted && !empty($tablePrivileges)) { + $query = $this->query( + 'SELECT COUNT(*) as matches' + . ' FROM information_schema.table_privileges' + . ' WHERE grantee = :grantee' + . ' AND table_schema = :dbname' + . ' AND table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')' + . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $tablePrivileges)) . ')' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee, ':dbname' => $this->config['dbname']) + ); + $expectedAmountOfMatches = count($context) * count($tablePrivileges); + $tablePrivilegesGranted = (int) $query->fetchObject()->matches === $expectedAmountOfMatches; + } + + if ($dbPrivilegesGranted && $tablePrivilegesGranted) { + return true; + } + } + + $query = $this->query( + 'SELECT COUNT(*) as matches FROM information_schema.user_privileges WHERE grantee = :grantee' + . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $mysqlPrivileges)) . ')' + . ($requireGrants ? " AND is_grantable = 'YES'" : ''), + array(':grantee' => $grantee) + ); + return (int) $query->fetchObject()->matches === count($mysqlPrivileges); + } + + /** + * Check whether the current user has the given privileges + * + * Note that database and table specific privileges (i.e. not SUPER, CREATE and CREATEROLE) are ignored + * in case no connection to the database defined in the resource configuration has been established + * + * @param array $privileges The privilege names + * @param bool $requireGrants Only return true when all privileges can be granted to others + * @param array $context An array describing the context for which the given privileges need to apply. + * Only one or more table names are currently supported + * @param string $username The login name to which the passed privileges need to be granted + * + * @return bool + */ + public function checkPgsqlPrivileges( + array $privileges, + $requireGrants = false, + array $context = null, + $username = null + ) { + $privilegesGranted = true; + if ($this->dbFromConfig) { + $dbPrivileges = array(); + $tablePrivileges = array(); + foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) { + if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) { + $tablePrivileges[] = $privilege; + } + if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) { + $dbPrivileges[] = $privilege; + } + } + + if (! empty($dbPrivileges)) { + $dbExclusivesGranted = true; + foreach ($dbPrivileges as $dbPrivilege) { + $query = $this->query( + 'SELECT has_database_privilege(:user, :dbname, :privilege) AS db_privilege_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':dbname' => $this->config['dbname'], + ':privilege' => $dbPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + if (! $query->fetchObject()->db_privilege_granted) { + $privilegesGranted = false; + if (! in_array($dbPrivilege, $tablePrivileges)) { + $dbExclusivesGranted = false; + } + } + } + + if ($privilegesGranted) { + // Do not check privileges twice if they are already granted at database level + $tablePrivileges = array_diff($tablePrivileges, $dbPrivileges); + } elseif ($dbExclusivesGranted) { + $privilegesGranted = true; + } + } + + if ($privilegesGranted && !empty($tablePrivileges)) { + foreach (array_intersect($context, $this->listTables()) as $table) { + foreach ($tablePrivileges as $tablePrivilege) { + $query = $this->query( + 'SELECT has_table_privilege(:user, :table, :privilege) AS table_privilege_granted', + array( + ':user' => $username !== null ? $username : $this->config['username'], + ':table' => $table, + ':privilege' => $tablePrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '') + ) + ); + $privilegesGranted &= $query->fetchObject()->table_privilege_granted; + } + } + } + } else { + // In case we cannot check whether the user got the required db-/table-privileges due to not being + // connected to the database defined in the resource configuration it is safe to just ignore them + // as the chances are very high that the database is created later causing the current user being + // the owner with ALL privileges. (Which in turn can be granted to others.) + + if (array_search('CREATE', $privileges, true) !== false) { + $query = $this->query( + 'select rolcreatedb from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + } + + if (array_search('CREATEROLE', $privileges, true) !== false) { + $query = $this->query( + 'select rolcreaterole from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + + if (array_search('SUPER', $privileges, true) !== false) { + $query = $this->query( + 'select rolsuper from pg_roles where rolname = :user', + array(':user' => $username !== null ? $username : $this->config['username']) + ); + $privilegesGranted &= $query->fetchColumn() !== false; + } + + return (bool) $privilegesGranted; + } +} diff --git a/modules/setup/library/Setup/Utils/EnableModuleStep.php b/modules/setup/library/Setup/Utils/EnableModuleStep.php new file mode 100644 index 0000000..92af5b7 --- /dev/null +++ b/modules/setup/library/Setup/Utils/EnableModuleStep.php @@ -0,0 +1,77 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Utils; + +use Exception; +use Icinga\Application\Icinga; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\IcingaException; +use Icinga\Module\Setup\Step; + +class EnableModuleStep extends Step +{ + protected $modulePaths; + + protected $moduleNames; + + protected $errors; + + protected $warnings; + + public function __construct(array $moduleNames) + { + $this->moduleNames = $moduleNames; + + $this->modulePaths = array(); + if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) { + $this->modulePaths[] = $appModulePath; + } + } + + public function apply() + { + $moduleManager = Icinga::app()->getModuleManager(); + $moduleManager->detectInstalledModules($this->modulePaths); + + $success = true; + foreach ($this->moduleNames as $moduleName) { + try { + $moduleManager->enableModule($moduleName); + } catch (ConfigurationError $e) { + $this->warnings[$moduleName] = $e; + } catch (Exception $e) { + $this->errors[$moduleName] = $e; + $success = false; + } + } + + return $success; + } + + public function getSummary() + { + // Enabling a module is like a implicit action, which does not need to be shown to the user... + } + + public function getReport() + { + $okMessage = mt('setup', 'Module "%s" has been successfully enabled.'); + $failMessage = mt('setup', 'Module "%s" could not be enabled. An error occured:'); + + $report = array(); + foreach ($this->moduleNames as $moduleName) { + if (isset($this->errors[$moduleName])) { + $report[] = sprintf($failMessage, $moduleName); + $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->errors[$moduleName])); + } elseif (isset($this->warnings[$moduleName])) { + $report[] = sprintf($failMessage, $moduleName); + $report[] = sprintf(mt('setup', 'WARNING: %s'), $this->warnings[$moduleName]->getMessage()); + } else { + $report[] = sprintf($okMessage, $moduleName); + } + } + + return $report; + } +} diff --git a/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php new file mode 100644 index 0000000..a3f218b --- /dev/null +++ b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Web\Form\Validator; + +use Exception; +use Zend_Validate_Abstract; +use Icinga\Util\File; + +/** + * Validator that checks if a token matches with the contents of a corresponding token-file + */ +class TokenValidator extends Zend_Validate_Abstract +{ + /** + * The path to the token file + * + * @var string + */ + protected $tokenPath; + + /** + * Create a new TokenValidator + * + * @param string $tokenPath The path to the token-file + */ + public function __construct($tokenPath) + { + $this->tokenPath = $tokenPath; + $this->_messageTemplates = array( + 'TOKEN_FILE_ERROR' => sprintf( + mt('setup', 'Cannot validate token: %s (%s)'), + $tokenPath, + '%value%' + ), + 'TOKEN_FILE_EMPTY' => sprintf( + mt('setup', 'Cannot validate token, file "%s" is empty. Please define a token.'), + $tokenPath + ), + 'TOKEN_INVALID' => mt('setup', 'Invalid token supplied.') + ); + } + + /** + * Validate the given token with the one in the token-file + * + * @param string $value The token to validate + * @param null $context The form context (ignored) + * + * @return bool + */ + public function isValid($value, $context = null) + { + try { + $file = new File($this->tokenPath); + $expectedToken = trim($file->fgets()); + } catch (Exception $e) { + $msg = $e->getMessage(); + $this->_error('TOKEN_FILE_ERROR', substr($msg, strpos($msg, ']: ') + 3)); + return false; + } + + if (empty($expectedToken)) { + $this->_error('TOKEN_FILE_EMPTY'); + return false; + } elseif ($value !== $expectedToken) { + $this->_error('TOKEN_INVALID'); + return false; + } + + return true; + } +} diff --git a/modules/setup/library/Setup/WebWizard.php b/modules/setup/library/Setup/WebWizard.php new file mode 100644 index 0000000..f25be55 --- /dev/null +++ b/modules/setup/library/Setup/WebWizard.php @@ -0,0 +1,752 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use Icinga\Application\Platform; +use Icinga\Module\Setup\Requirement\SetRequirement; +use Icinga\Module\Setup\Requirement\WebLibraryRequirement; +use PDOException; +use Icinga\Web\Form; +use Icinga\Web\Wizard; +use Icinga\Web\Request; +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Module\Setup\Forms\ModulePage; +use Icinga\Module\Setup\Forms\WelcomePage; +use Icinga\Module\Setup\Forms\SummaryPage; +use Icinga\Module\Setup\Forms\DbResourcePage; +use Icinga\Module\Setup\Forms\AuthBackendPage; +use Icinga\Module\Setup\Forms\AdminAccountPage; +use Icinga\Module\Setup\Forms\LdapDiscoveryPage; +//use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage; +use Icinga\Module\Setup\Forms\LdapResourcePage; +use Icinga\Module\Setup\Forms\RequirementsPage; +use Icinga\Module\Setup\Forms\GeneralConfigPage; +use Icinga\Module\Setup\Forms\AuthenticationPage; +use Icinga\Module\Setup\Forms\DatabaseCreationPage; +use Icinga\Module\Setup\Forms\UserGroupBackendPage; +use Icinga\Module\Setup\Steps\DatabaseStep; +use Icinga\Module\Setup\Steps\GeneralConfigStep; +use Icinga\Module\Setup\Steps\ResourceStep; +use Icinga\Module\Setup\Steps\AuthenticationStep; +use Icinga\Module\Setup\Steps\UserGroupStep; +use Icinga\Module\Setup\Utils\EnableModuleStep; +use Icinga\Module\Setup\Utils\DbTool; +use Icinga\Module\Setup\Requirement\OSRequirement; +use Icinga\Module\Setup\Requirement\PhpModuleRequirement; +use Icinga\Module\Setup\Requirement\PhpVersionRequirement; +use Icinga\Module\Setup\Requirement\ConfigDirectoryRequirement; +use Icinga\Module\Monitoring\Forms\Config\Transport\ApiTransportForm; + +/** + * Icinga Web 2 Setup Wizard + */ +class WebWizard extends Wizard implements SetupWizard +{ + /** + * The privileges required by Icinga Web 2 to create the database and a login + * + * @var array + */ + protected $databaseCreationPrivileges = array( + 'CREATE', + 'CREATE USER', // MySQL + 'CREATEROLE' // PostgreSQL + ); + + /** + * The privileges required by Icinga Web 2 to setup the database + * + * @var array + */ + protected $databaseSetupPrivileges = array( + 'CREATE', + 'ALTER', // MySQL only + 'REFERENCES' + ); + + /** + * The privileges required by Icinga Web 2 to operate the database + * + * @var array + */ + protected $databaseUsagePrivileges = array( + 'SELECT', + 'INSERT', + 'UPDATE', + 'DELETE', + 'EXECUTE', + 'TEMPORARY', // PostgreSql + 'CREATE TEMPORARY TABLES' // MySQL + ); + + /** + * The database tables operated by Icinga Web 2 + * + * @var array + */ + protected $databaseTables = array( + 'icingaweb_group', + 'icingaweb_group_membership', + 'icingaweb_user', + 'icingaweb_user_preference', + 'icingaweb_rememberme' + ); + + /** + * Register all pages and module wizards for this wizard + */ + protected function init() + { + $this->addPage(new WelcomePage()); + $this->addPage(new ModulePage()); + $this->addPage(new RequirementsPage()); + $this->addPage(new AuthenticationPage()); + $this->addPage(new DbResourcePage(array('name' => 'setup_auth_db_resource'))); + $this->addPage(new DatabaseCreationPage(array('name' => 'setup_auth_db_creation'))); + $this->addPage(new LdapDiscoveryPage()); + //$this->addPage(new LdapDiscoveryConfirmPage()); + $this->addPage(new LdapResourcePage()); + $this->addPage(new AuthBackendPage()); + $this->addPage(new UserGroupBackendPage()); + $this->addPage(new AdminAccountPage()); + $this->addPage(new GeneralConfigPage()); + $this->addPage(new DbResourcePage(array('name' => 'setup_config_db_resource'))); + $this->addPage(new DatabaseCreationPage(array('name' => 'setup_config_db_creation'))); + $this->addPage(new SummaryPage(array('name' => 'setup_summary'))); + + if (($modulePageData = $this->getPageData('setup_modules')) !== null) { + $modulePage = $this->getPage('setup_modules')->populate($modulePageData); + foreach ($modulePage->getModuleWizards() as $moduleWizard) { + $this->addPage($moduleWizard); + } + } + } + + /** + * Setup the given page that is either going to be displayed or validated + * + * @param Form $page The page to setup + * @param Request $request The current request + */ + public function setupPage(Form $page, Request $request) + { + if ($page->getName() === 'setup_requirements') { + $page->setWizard($this); + } elseif ($page->getName() === 'setup_authentication_backend') { + /** @var AuthBackendPage $page */ + + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->setResourceConfig($this->getPageData('setup_auth_db_resource')); + } elseif ($authData['type'] === 'ldap') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + + $suggestions = $this->getPageData('setup_ldap_discovery'); + if (isset($suggestions['backend'])) { + $page->setSuggestions($suggestions['backend']); + } + + if ($this->getDirection() === static::FORWARD) { + $backendConfig = $this->getPageData('setup_authentication_backend'); + if ($backendConfig !== null && $request->getPost('name') !== $backendConfig['name']) { + $pageData = & $this->getPageData(); + unset($pageData['setup_usergroup_backend']); + } + } + } + + if ($this->getDirection() === static::FORWARD) { + $backendConfig = $this->getPageData('setup_authentication_backend'); + if ($backendConfig !== null && $request->getPost('backend') !== $backendConfig['backend']) { + $pageData = & $this->getPageData(); + unset($pageData['setup_usergroup_backend']); + } + } + /*} elseif ($page->getName() === 'setup_ldap_discovery_confirm') { + $page->setResourceConfig($this->getPageData('setup_ldap_discovery'));*/ + } elseif ($page->getName() === 'setup_auth_db_resource') { + $page->addDescription(mt( + 'setup', + 'Now please configure the database resource where to store users and user groups.' + )); + $page->addDescription(mt( + 'setup', + 'Note that the database itself does not need to exist at this time as' + . ' it is going to be created once the wizard is about to be finished.' + )); + } elseif ($page->getName() === 'setup_usergroup_backend') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + $page->setBackendConfig($this->getPageData('setup_authentication_backend')); + } elseif ($page->getName() === 'setup_admin_account') { + $page->setBackendConfig($this->getPageData('setup_authentication_backend')); + $page->setGroupConfig($this->getPageData('setup_usergroup_backend')); + $authData = $this->getPageData('setup_authentication_type'); + if ($authData['type'] === 'db') { + $page->setResourceConfig($this->getPageData('setup_auth_db_resource')); + } elseif ($authData['type'] === 'ldap') { + $page->setResourceConfig($this->getPageData('setup_ldap_resource')); + } + } elseif ($page->getName() === 'setup_auth_db_creation' || $page->getName() === 'setup_config_db_creation') { + $page->setDatabaseSetupPrivileges( + array_unique(array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges)) + ); + $page->setDatabaseUsagePrivileges($this->databaseUsagePrivileges); + $page->setResourceConfig( + $this->getPageData('setup_auth_db_resource') ?: $this->getPageData('setup_config_db_resource') + ); + } elseif ($page->getName() === 'setup_summary') { + $page->setSubjectTitle('Icinga Web 2'); + $page->setSummary($this->getSetup()->getSummary()); + } elseif ($page->getName() === 'setup_config_db_resource') { + $page->addDescription(mt( + 'setup', + 'Now please configure the database resource where to store user preferences.' + )); + $page->addDescription(mt( + 'setup', + 'Note that the database itself does not need to exist at this time as' + . ' it is going to be created once the wizard is about to be finished.' + )); + + $ldapData = $this->getPageData('setup_ldap_resource'); + if ($ldapData !== null && $request->getPost('name') === $ldapData['name']) { + $page->error( + mt('setup', 'The given resource name must be unique and is already in use by the LDAP resource') + ); + } + } elseif ($page->getName() === 'setup_ldap_resource') { + $suggestion = $this->getPageData('setup_ldap_discovery'); + if (isset($suggestion['resource'])) { + $page->populate($suggestion['resource']); + } + + if ($this->getDirection() === static::FORWARD) { + $resourceConfig = $this->getPageData('setup_ldap_resource'); + if ($resourceConfig !== null && $request->getPost('name') !== $resourceConfig['name']) { + $pageData = & $this->getPageData(); + unset($pageData['setup_usergroup_backend']); + } + } + } elseif ($page->getName() === 'setup_authentication_type') { + $authData = $this->getPageData($page->getName()); + $pageData = & $this->getPageData(); + + if ($authData !== null && $request->getPost('type') !== $authData['type']) { + // Drop any existing page data in case the authentication type has changed, + // otherwise it will conflict with other forms that depend on this one + unset($pageData['setup_admin_account']); + unset($pageData['setup_authentication_backend']); + + if ($authData['type'] === 'db') { + unset($pageData['setup_auth_db_resource']); + unset($pageData['setup_auth_db_creation']); + } elseif ($request->getPost('type') === 'db') { + unset($pageData['setup_config_db_resource']); + unset($pageData['setup_config_db_creation']); + } + } elseif (isset($authData['type']) && $authData['type'] == 'external') { + // If you choose the authentication type external and validate the database and then come + // back to change the authentication type but do not change it, you will get an database configuration + // related error message on the next page. To avoid this error, the 'setup_config_db_resource' + // page must be unset. + + unset($pageData['setup_config_db_resource']); + } + } + } + + /** + * Return the new page to set as current page + * + * {@inheritdoc} Runs additional checks related to some registered pages. + * + * @param string $requestedPage The name of the requested page + * @param Form $originPage The origin page + * + * @return Form The new page + * + * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet + */ + protected function getNewPage($requestedPage, Form $originPage) + { + $skip = false; + $newPage = parent::getNewPage($requestedPage, $originPage); + if ($newPage->getName() === 'setup_auth_db_resource') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] !== 'db'; + } elseif ($newPage->getname() === 'setup_ldap_discovery') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] !== 'ldap'; + /*} elseif ($newPage->getName() === 'setup_ldap_discovery_confirm') { + $skip = false === $this->hasPageData('setup_ldap_discovery');*/ + } elseif ($newPage->getName() === 'setup_ldap_resource') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] !== 'ldap'; + } elseif ($newPage->getName() === 'setup_usergroup_backend') { + $backendConfig = $this->getPageData('setup_authentication_backend'); + $skip = $backendConfig['backend'] !== 'ldap'; + } elseif ($newPage->getName() === 'setup_config_db_resource') { + $authData = $this->getPageData('setup_authentication_type'); + $skip = $authData['type'] === 'db'; + } elseif (in_array($newPage->getName(), array('setup_auth_db_creation', 'setup_config_db_creation'))) { + if (($newPage->getName() === 'setup_auth_db_creation' || $this->hasPageData('setup_config_db_resource')) + && (($config = $this->getPageData('setup_auth_db_resource')) !== null + || ($config = $this->getPageData('setup_config_db_resource')) !== null) + && !$config['skip_validation'] && $this->getDirection() == static::FORWARD + ) { + // Execute this code only if the direction is forward. + // Otherwise, an error will be output when you go back. + $db = new DbTool($config); + + try { + $db->connectToDb(); // Are we able to login on the database? + + if (array_search(reset($this->databaseTables), $db->listTables(), true) === false) { + // In case the database schema does not yet exist the + // user needs the privileges to setup the database + $skip = $db->checkPrivileges($this->databaseSetupPrivileges, $this->databaseTables); + } else { + // In case the database schema exists the user needs the required privileges + // to operate the database, if those are missing we ask for another user + $skip = $db->checkPrivileges($this->databaseUsagePrivileges, $this->databaseTables); + } + } catch (PDOException $_) { + try { + $db->connectToHost(); // Are we able to login on the server? + // It is not possible to reliably determine whether a database exists or not if a user can't + // log in to the database, so we just require the user to be able to create the database + $skip = $db->checkPrivileges( + array_unique( + array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges) + ), + $this->databaseTables + ); + } catch (PDOException $_) { + // We are NOT able to login on the server.. + } + } + } else { + $skip = true; + } + } + + return $skip ? $this->skipPage($newPage) : $newPage; + } + + /** + * Add buttons to the given page based on its position in the page-chain + * + * @param Form $page The page to add the buttons to + */ + protected function addButtons(Form $page) + { + parent::addButtons($page); + + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('setup', 'Start', 'setup.welcome.btn.next') + ); + } elseif ($index === count($pages) - 1) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('setup', 'Setup Icinga Web 2', 'setup.summary.btn.finish') + ); + } + + $authData = $this->getPageData('setup_authentication_type'); + $veto = $page->getName() === 'setup_authentication_backend' && $authData['type'] === 'db'; + if (! $veto && in_array($page->getName(), array( + 'setup_authentication_backend', + 'setup_auth_db_resource', + 'setup_config_db_resource', + 'setup_ldap_resource', + 'setup_monitoring_ido', + 'setup_icingadb_resource', + 'setup_icingadb_redis', + 'setup_icingadb_api_transport' + ))) { + $page->addElement( + 'submit', + 'backend_validation', + array( + 'ignore' => true, + 'label' => t('Validate Configuration'), + 'data-progress-label' => t('Validation In Progress'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->getDisplayGroup('buttons')->addElement($page->getElement('backend_validation')); + } + + if ($page->getName() === 'setup_command_transport') { + if ($page->getSubForm('transport_form')->getSubForm('transport_form') instanceof ApiTransportForm) { + $page->addElement( + 'submit', + 'transport_validation', + array( + 'ignore' => true, + 'label' => t('Validate Configuration'), + 'data-progress-label' => t('Validation In Progress'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->getDisplayGroup('buttons')->addElement($page->getElement('transport_validation')); + } + } + } + + /** + * Clear the session being used by this wizard + * + * @param bool $removeToken If true, the setup token will be removed + */ + public function clearSession($removeToken = true) + { + parent::clearSession(); + + if ($removeToken) { + $tokenPath = Config::resolvePath('setup.token'); + if (file_exists($tokenPath)) { + @unlink($tokenPath); + } + } + } + + /** + * Return the setup for this wizard + * + * @return Setup + */ + public function getSetup() + { + $pageData = $this->getPageData(); + $setup = new Setup(); + + if (isset($pageData['setup_auth_db_resource']) + && !$pageData['setup_auth_db_resource']['skip_validation'] + && (! isset($pageData['setup_auth_db_creation']) + || !$pageData['setup_auth_db_creation']['skip_validation'] + ) + ) { + $setup->addStep( + new DatabaseStep(array( + 'tables' => $this->databaseTables, + 'privileges' => $this->databaseUsagePrivileges, + 'resourceConfig' => $pageData['setup_auth_db_resource'], + 'adminName' => isset($pageData['setup_auth_db_creation']['username']) + ? $pageData['setup_auth_db_creation']['username'] + : null, + 'adminPassword' => isset($pageData['setup_auth_db_creation']['password']) + ? $pageData['setup_auth_db_creation']['password'] + : null, + 'schemaPath' => Config::module('setup') + ->get('schema', 'path', Icinga::app()->getBaseDir('schema')) + )) + ); + } elseif (isset($pageData['setup_config_db_resource']) + && !$pageData['setup_config_db_resource']['skip_validation'] + && (! isset($pageData['setup_config_db_creation']) + || !$pageData['setup_config_db_creation']['skip_validation'] + ) + ) { + $setup->addStep( + new DatabaseStep(array( + 'tables' => $this->databaseTables, + 'privileges' => $this->databaseUsagePrivileges, + 'resourceConfig' => $pageData['setup_config_db_resource'], + 'adminName' => isset($pageData['setup_config_db_creation']['username']) + ? $pageData['setup_config_db_creation']['username'] + : null, + 'adminPassword' => isset($pageData['setup_config_db_creation']['password']) + ? $pageData['setup_config_db_creation']['password'] + : null, + 'schemaPath' => Config::module('setup') + ->get('schema', 'path', Icinga::app()->getBaseDir('schema')) + )) + ); + } + + $setup->addStep( + new GeneralConfigStep(array( + 'generalConfig' => $pageData['setup_general_config'], + 'resourceName' => isset($pageData['setup_auth_db_resource']['name']) + ? $pageData['setup_auth_db_resource']['name'] + : (isset($pageData['setup_config_db_resource']['name']) + ? $pageData['setup_config_db_resource']['name'] + : null + ) + )) + ); + + $adminAccountType = $pageData['setup_admin_account']['user_type']; + if ($adminAccountType === 'user_group') { + $adminAccountData = array('groupname' => $pageData['setup_admin_account'][$adminAccountType]); + } else { + $adminAccountData = array('username' => $pageData['setup_admin_account'][$adminAccountType]); + if ($adminAccountType === 'new_user' && !$pageData['setup_auth_db_resource']['skip_validation'] + && (! isset($pageData['setup_auth_db_creation']) + || !$pageData['setup_auth_db_creation']['skip_validation'] + ) + ) { + $adminAccountData['resourceConfig'] = $pageData['setup_auth_db_resource']; + $adminAccountData['password'] = $pageData['setup_admin_account']['new_user_password']; + } + } + $authType = $pageData['setup_authentication_type']['type']; + $setup->addStep( + new AuthenticationStep(array( + 'adminAccountData' => $adminAccountData, + 'backendConfig' => $pageData['setup_authentication_backend'], + 'resourceName' => $authType === 'db' ? $pageData['setup_auth_db_resource']['name'] : ( + $authType === 'ldap' ? $pageData['setup_ldap_resource']['name'] : null + ) + )) + ); + + if ($authType !== 'external') { + $setup->addStep( + new UserGroupStep(array( + 'backendConfig' => $pageData['setup_authentication_backend'], + 'groupConfig' => isset($pageData['setup_usergroup_backend']) + ? $pageData['setup_usergroup_backend'] + : null, + 'resourceName' => $authType === 'db' + ? $pageData['setup_auth_db_resource']['name'] + : $pageData['setup_ldap_resource']['name'], + 'resourceConfig' => $authType === 'db' + ? $pageData['setup_auth_db_resource'] + : null, + 'username' => $authType === 'db' + ? $pageData['setup_admin_account'][$adminAccountType] + : null + )) + ); + } + + if (isset($pageData['setup_auth_db_resource']) + || isset($pageData['setup_config_db_resource']) + || isset($pageData['setup_ldap_resource']) + ) { + $setup->addStep( + new ResourceStep(array( + 'dbResourceConfig' => isset($pageData['setup_auth_db_resource']) + ? array_diff_key($pageData['setup_auth_db_resource'], array('skip_validation' => null)) + : (isset($pageData['setup_config_db_resource']) + ? array_diff_key($pageData['setup_config_db_resource'], array('skip_validation' => null)) + : null + ), + 'ldapResourceConfig' => isset($pageData['setup_ldap_resource']) + ? array_diff_key($pageData['setup_ldap_resource'], array('skip_validation' => null)) + : null + )) + ); + } + + foreach ($this->getWizards() as $wizard) { + if ($wizard->isComplete()) { + $setup->addSteps($wizard->getSetup()->getSteps()); + } + } + + $setup->addStep(new EnableModuleStep(array_keys($this->getPage('setup_modules')->getCheckedModules()))); + + return $setup; + } + + /** + * Return the requirements of this wizard + * + * @return RequirementSet + */ + public function getRequirements($skipModules = false) + { + $set = new RequirementSet(); + + $set->add(new PhpVersionRequirement(array( + 'condition' => array('>=', '7.2'), + 'description' => sprintf(mt( + 'setup', + 'Running Icinga Web 2 requires PHP version %s.' + ), '7.2') + ))); + + $set->add(new OSRequirement(array( + 'optional' => true, + 'condition' => 'linux', + 'description' => mt( + 'setup', + 'Icinga Web 2 is developed for and tested on Linux. While we cannot' + . ' guarantee they will, other platforms may also perform as well.' + ) + ))); + + $set->add(new WebLibraryRequirement(array( + 'condition' => ['icinga-php-library', '>=', '0.9.0'], + 'alias' => 'Icinga PHP library', + 'description' => mt( + 'setup', + 'The Icinga PHP library (IPL) is required for Icinga Web 2 and modules' + ) + ))); + + $set->add(new WebLibraryRequirement(array( + 'condition' => ['icinga-php-thirdparty', '>=', '0.11.0'], + 'alias' => 'Icinga PHP Thirdparty', + 'description' => mt( + 'setup', + 'The Icinga PHP Thirdparty library is required for Icinga Web 2 and modules' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'OpenSSL', + 'description' => mt( + 'setup', + 'The PHP module for OpenSSL is required to generate cryptographically safe password salts.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'XML', + 'description' => mt( + 'setup', + 'The XML module for PHP is required for Markdown and custom HTML annotations.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'JSON', + 'description' => mt( + 'setup', + 'The JSON module for PHP is required for various export functionalities as well as APIs.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'gettext', + 'description' => mt( + 'setup', + 'For message localization, the gettext module for PHP is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'INTL', + 'description' => mt( + 'setup', + 'For language, timezone and date/time format negotiation, the INTL module for PHP is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'condition' => 'DOM', + 'description' => mt( + 'setup', + 'For charts and exports of views and reports to PDF, the DOM module for PHP is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'LDAP', + 'description' => mt( + 'setup', + 'If you\'d like to authenticate users using LDAP the corresponding PHP module is required.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'mbstring', + 'description' => mt( + 'setup', + 'In case you want views being exported to PDF, you\'ll need the mbstring extension for PHP.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'GD', + 'description' => mt( + 'setup', + 'In case you want views being exported to PDF, you\'ll need the GD extension for PHP.' + ) + ))); + + $set->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'Imagick', + 'description' => mt( + 'setup', + 'In case you want graphs being exported to PDF as well, you\'ll need the ImageMagick extension for PHP.' + ) + ))); + + $dbSet = new RequirementSet(false, RequirementSet::MODE_OR); + $dbSet->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'pdo_mysql', + 'alias' => 'PDO-MySQL', + 'description' => mt( + 'setup', + 'To store users or preferences in a MySQL database the PDO-MySQL module for PHP is required.' + ) + ))); + $dbSet->add(new PhpModuleRequirement(array( + 'optional' => true, + 'condition' => 'pdo_pgsql', + 'alias' => 'PDO-PostgreSQL', + 'description' => mt( + 'setup', + 'To store users or preferences in a PostgreSQL database the PDO-PostgreSQL module for PHP is required.' + ) + ))); + $set->merge($dbSet); + + $dbRequire = (new SetRequirement(array( + 'optional' => false, + 'condition' => $dbSet, + 'title' =>'Database', + 'alias' => 'PDO-MySQL OR PDO-PostgreSQL', + 'description' => mt( + 'setup', + 'A database is mandatory, therefore at least one module ' + . 'PDO-MySQL or PDO-PostgreSQL for PHP is required.' + ) + ))); + + $set->add($dbRequire); + + $set->add(new ConfigDirectoryRequirement(array( + 'condition' => Icinga::app()->getStorageDir(), + 'title' => mt('setup', 'Read- and writable storage directory'), + 'description' => mt( + 'setup', + 'The Icinga Web 2 storage directory defaults to "/var/lib/icingaweb2", if' . + ' not explicitly set in the environment variable "ICINGAWEB_STORAGEDIR".' + ) + ))); + + $set->add(new ConfigDirectoryRequirement(array( + 'condition' => Icinga::app()->getConfigDir(), + 'description' => mt( + 'setup', + 'The Icinga Web 2 configuration directory defaults to "/etc/icingaweb2", if' . + ' not explicitly set in the environment variable "ICINGAWEB_CONFIGDIR".' + ) + ))); + + if (! $skipModules) { + foreach ($this->getWizards() as $wizard) { + $set->merge($wizard->getRequirements()); + } + } + + return $set; + } +} diff --git a/modules/setup/library/Setup/Webserver.php b/modules/setup/library/Setup/Webserver.php new file mode 100644 index 0000000..2251ba3 --- /dev/null +++ b/modules/setup/library/Setup/Webserver.php @@ -0,0 +1,233 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup; + +use Icinga\Application\Icinga; +use Icinga\Exception\ProgrammingError; + +/** + * Base class for generating webserver configuration + */ +abstract class Webserver +{ + /** + * Document root + * + * @var string + */ + protected $documentRoot; + + /** + * URL path of Icinga Web 2 + * + * @var string + */ + protected $urlPath = '/icingaweb2'; + + /** + * Path to Icinga Web 2's configuration files + * + * @var string + */ + protected $configDir; + + /** + * Address or path where to pass requests to FPM + * + * @var string + */ + protected $fpmUri; + + /** + * Enable to pass requests to FPM + * + * @var bool + */ + protected $enableFpm = false; + + /** + * Create instance by type name + * + * @param string $type + * + * @return WebServer + * + * @throws ProgrammingError + */ + public static function createInstance($type) + { + $class = __NAMESPACE__ . '\\Webserver\\' . ucfirst($type); + if (class_exists($class)) { + return new $class(); + } + throw new ProgrammingError('Class "%s" does not exist', $class); + } + + /** + * Generate configuration + * + * @return string + */ + public function generate() + { + $template = $this->getTemplate(); + + $searchTokens = array( + '{urlPath}', + '{documentRoot}', + '{aliasDocumentRoot}', + '{configDir}', + '{fpmUri}' + ); + $replaceTokens = array( + $this->getUrlPath(), + $this->getDocumentRoot(), + preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(), + $this->getConfigDir(), + $this->getFpmUri() + ); + $template = str_replace($searchTokens, $replaceTokens, $template); + return $template; + } + + /** + * Specific template + * + * @return string + */ + abstract protected function getTemplate(); + + /** + * Set the URL path of Icinga Web 2 + * + * @param string $urlPath + * + * @return $this + */ + public function setUrlPath($urlPath) + { + $this->urlPath = '/' . ltrim(trim((string) $urlPath), '/'); + return $this; + } + + /** + * Get the URL path of Icinga Web 2 + * + * @return string + */ + public function getUrlPath() + { + return $this->urlPath; + } + + /** + * Set the document root + * + * @param string $documentRoot + * + * @return $this + */ + public function setDocumentRoot($documentRoot) + { + $this->documentRoot = trim((string) $documentRoot); + return $this; + } + + /** + * Detect the document root + * + * @return string + */ + public function detectDocumentRoot() + { + return Icinga::app()->getBaseDir('public'); + } + + /** + * Get the document root + * + * @return string + */ + public function getDocumentRoot() + { + if ($this->documentRoot === null) { + $this->documentRoot = $this->detectDocumentRoot(); + } + return $this->documentRoot; + } + + /** + * Set the configuration directory + * + * @param string $configDir + * + * @return $this + */ + public function setConfigDir($configDir) + { + $this->configDir = (string) $configDir; + return $this; + } + + /** + * Get the configuration directory + * + * @return string + */ + public function getConfigDir() + { + if ($this->configDir === null) { + return Icinga::app()->getConfigDir(); + } + return $this->configDir; + } + + /** + * Get whether FPM is enabled + * + * @return bool + */ + public function getEnableFpm() + { + return $this->enableFpm; + } + + /** + * Set FPM enabled + * + * @param bool $flag + * + * @return $this + */ + public function setEnableFpm($flag) + { + $this->enableFpm = (bool) $flag; + + return $this; + } + + /** + * Get the address or path where to pass requests to FPM + * + * @return string + */ + public function getFpmUri() + { + return $this->fpmUri; + } + + /** + * Set the address or path where to pass requests to FPM + * + * @param string $uri + * + * @return $this + */ + public function setFpmUri($uri) + { + $this->fpmUri = (string) $uri; + + return $this; + } +} diff --git a/modules/setup/library/Setup/Webserver/Apache.php b/modules/setup/library/Setup/Webserver/Apache.php new file mode 100644 index 0000000..fdb367f --- /dev/null +++ b/modules/setup/library/Setup/Webserver/Apache.php @@ -0,0 +1,142 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Webserver; + +use Icinga\Module\Setup\Webserver; + +/** + * Generate Apache 2.x configuration + */ +class Apache extends Webserver +{ + protected $fpmUri = '127.0.0.1:9000'; + + protected function getTemplate() + { + if (! $this->enableFpm) { + return <<<'EOD' +Alias {urlPath} "{aliasDocumentRoot}" + +# Remove comments if you want to use PHP FPM and your Apache version is older than 2.4 +#<IfVersion < 2.4> +# # Forward PHP requests to FPM +# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +# <LocationMatch "^{urlPath}/(.*\.php)$"> +# ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1" +# </LocationMatch> +#</IfVersion> + +<Directory "{documentRoot}"> + Options SymLinksIfOwnerMatch + AllowOverride None + + DirectoryIndex index.php + + <IfModule mod_authz_core.c> + # Apache 2.4 + <RequireAll> + Require all granted + </RequireAll> + </IfModule> + + <IfModule !mod_authz_core.c> + # Apache 2.2 + Order allow,deny + Allow from all + </IfModule> + + SetEnv ICINGAWEB_CONFIGDIR "{configDir}" + + EnableSendfile Off + + <IfModule mod_rewrite.c> + RewriteEngine on + RewriteBase {urlPath}/ + RewriteCond %{REQUEST_FILENAME} -s [OR] + RewriteCond %{REQUEST_FILENAME} -l [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^.*$ - [NC,L] + RewriteRule ^.*$ index.php [NC,L] + </IfModule> + + <IfModule !mod_rewrite.c> + DirectoryIndex error_norewrite.html + ErrorDocument 404 {urlPath}/error_norewrite.html + </IfModule> + +# Remove comments if you want to use PHP FPM and your Apache version +# is greater than or equal to 2.4 +# <IfVersion >= 2.4> +# # Forward PHP requests to FPM +# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +# <FilesMatch "\.php$"> +# SetHandler "proxy:fcgi://{fpmUri}" +# ErrorDocument 503 {urlPath}/error_unavailable.html +# </FilesMatch> +# </IfVersion> +</Directory> +EOD; + } else { + return <<<'EOD' +Alias {urlPath} "{aliasDocumentRoot}" + +<IfVersion < 2.4> + # Forward PHP requests to FPM + SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + <LocationMatch "^{urlPath}/(.*\.php)$"> + ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1" + </LocationMatch> +</IfVersion> + +<Directory "{documentRoot}"> + Options SymLinksIfOwnerMatch + AllowOverride None + + DirectoryIndex index.php + + <IfModule mod_authz_core.c> + # Apache 2.4 + <RequireAll> + Require all granted + </RequireAll> + </IfModule> + + <IfModule !mod_authz_core.c> + # Apache 2.2 + Order allow,deny + Allow from all + </IfModule> + + SetEnv ICINGAWEB_CONFIGDIR "{configDir}" + + EnableSendfile Off + + <IfModule mod_rewrite.c> + RewriteEngine on + RewriteBase {urlPath}/ + RewriteCond %{REQUEST_FILENAME} -s [OR] + RewriteCond %{REQUEST_FILENAME} -l [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^.*$ - [NC,L] + RewriteRule ^.*$ index.php [NC,L] + </IfModule> + + <IfModule !mod_rewrite.c> + DirectoryIndex error_norewrite.html + ErrorDocument 404 {urlPath}/error_norewrite.html + </IfModule> + + <IfVersion >= 2.4> + # Forward PHP requests to FPM + SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 + <FilesMatch "\.php$"> + SetHandler "proxy:fcgi://{fpmUri}" + ErrorDocument 503 {urlPath}/error_unavailable.html + </FilesMatch> + </IfVersion> +</Directory> +EOD; + } + } +} diff --git a/modules/setup/library/Setup/Webserver/Nginx.php b/modules/setup/library/Setup/Webserver/Nginx.php new file mode 100644 index 0000000..c7ae716 --- /dev/null +++ b/modules/setup/library/Setup/Webserver/Nginx.php @@ -0,0 +1,36 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Setup\Webserver; + +use Icinga\Module\Setup\Webserver; + +/** + * Generate nginx configuration + */ +class Nginx extends Webserver +{ + protected $fpmUri = '127.0.0.1:9000'; + + protected $enableFpm = true; + + protected function getTemplate() + { + return <<<'EOD' +location ~ ^{urlPath}/index\.php(.*)$ { + fastcgi_pass {fpmUri}; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME {documentRoot}/index.php; + fastcgi_param ICINGAWEB_CONFIGDIR {configDir}; + fastcgi_param REMOTE_USER $remote_user; +} + +location ~ ^{urlPath}(.+)? { + alias {documentRoot}; + index index.php; + try_files $1 $uri $uri/ {urlPath}/index.php$is_args$args; +} +EOD; + } +} diff --git a/modules/setup/module.info b/modules/setup/module.info new file mode 100644 index 0000000..e3570bd --- /dev/null +++ b/modules/setup/module.info @@ -0,0 +1,6 @@ +Module: setup +Version: 2.11.4 +Description: Setup module + Web based wizard for setting up Icinga Web 2 and its modules. + This includes the data backends (e.g. relational database, LDAP), + the authentication method, where to store the user preferences and much more. diff --git a/modules/translation/application/clicommands/CompileCommand.php b/modules/translation/application/clicommands/CompileCommand.php new file mode 100644 index 0000000..8408009 --- /dev/null +++ b/modules/translation/application/clicommands/CompileCommand.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Clicommands; + +use Icinga\Module\Translation\Cli\TranslationCommand; + +/** + * Translation compiler + * + * This command will compile gettext catalogs of modules. + * + * Once a catalog is compiled its content is used by Icinga Web 2 to display + * messages in the configured language. + */ +class CompileCommand extends TranslationCommand +{ + /** + * Compile a module gettext catalog + * + * This will compile the catalog of the given module and locale. + * + * USAGE: + * + * icingacli translation compile <module> <locale> + * + * EXAMPLES: + * + * icingacli translation compile demo de_DE + * icingacli translation compile demo fr_FR + */ + public function moduleAction() + { + $module = $this->validateModuleName($this->params->shift()); + $locale = $this->validateLocaleCode($this->params->shift()); + + $helper = $this->getTranslationHelper($locale); + $helper->compileModuleTranslation($module); + } +} diff --git a/modules/translation/application/clicommands/RefreshCommand.php b/modules/translation/application/clicommands/RefreshCommand.php new file mode 100644 index 0000000..b4b2dc0 --- /dev/null +++ b/modules/translation/application/clicommands/RefreshCommand.php @@ -0,0 +1,40 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Clicommands; + +use Icinga\Module\Translation\Cli\TranslationCommand; + +/** + * Translation updater + * + * This command will create a new or update any existing gettext catalog of a module. + * + * Once a catalog has been created/updated one can open it with a editor for + * PO-files and start with the actual translation. + */ +class RefreshCommand extends TranslationCommand +{ + /** + * Generate or update a module gettext catalog + * + * This will create/update the PO-file of the given module and locale. + * + * USAGE: + * + * icingacli translation refresh module <module> <locale> + * + * EXAMPLES: + * + * icingacli translation refresh module demo de_DE + * icingacli translation refresh module demo fr_FR + */ + public function moduleAction() + { + $module = $this->validateModuleName($this->params->shift()); + $locale = $this->validateLocaleCode($this->params->shift()); + + $helper = $this->getTranslationHelper($locale); + $helper->updateModuleTranslations($module); + } +} diff --git a/modules/translation/application/clicommands/TestCommand.php b/modules/translation/application/clicommands/TestCommand.php new file mode 100644 index 0000000..347c2c9 --- /dev/null +++ b/modules/translation/application/clicommands/TestCommand.php @@ -0,0 +1,140 @@ +<?php +/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Clicommands; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Translation\Cli\ArrayToTextTableHelper; +use Icinga\Module\Translation\Cli\TranslationCommand; +use ipl\I18n\GettextTranslator; +use ipl\I18n\StaticTranslator; + +/** + * Timestamp test helper + * + * + */ +class TestCommand extends TranslationCommand +{ + protected $locales = array(); + + /** + * Get translation examples for DateFormatter + * + * To help you check if the values got translated correctly + * + * USAGE: + * + * icingacli translation test dateformatter <locale> + * + * EXAMPLES: + * + * icingacli translation test dateformatter de_DE + * icingacli translation test dateformatter fr_FR + */ + public function dateformatterAction() + { + $time = time(); + + /** @uses DateFormatter::timeAgo */ + $this->printTable($this->getMultiTranslated( + 'Time Ago', + array('Icinga\Date\DateFormatter', 'timeAgo'), + array( + "15 sec" => $time - 15, + "62 sec" => $time - 62, + "10 min" => $time - 600, + "1h" => $time - 1 * 3600, + "3h" => $time - 3 * 3600, + "25h" => $time - 25 * 3600, + "31d" => $time - 31 * 24 * 3600, + ) + )); + + $this->printTable($this->getMultiTranslated( + 'Time Since', + array('Icinga\Date\DateFormatter', 'timeSince'), + array( + "15 sec" => $time - 15, + "62 sec" => $time - 62, + "10 min" => $time - 600, + "1h" => $time - 1 * 3600, + "3h" => $time - 3 * 3600, + "25h" => $time - 25 * 3600, + "31d" => $time - 31 * 24 * 3600, + ) + )); + + $this->printTable($this->getMultiTranslated( + 'Time Until', + array('Icinga\Date\DateFormatter', 'timeUntil'), + array( + "15 sec" => $time + 15, + "62 sec" => $time + 62, + "10 min" => $time + 600, + "1h" => $time + 1 * 3600, + "3h" => $time + 3 * 3600, + "25h" => $time + 25 * 3600, + "31d" => $time + 31 * 24 * 3600, + ) + )); + } + + public function defaultAction() + { + $this->dateformatterAction(); + } + + public function init() + { + foreach ($this->params->getAllStandalone() as $l) { + $this->locales[] = $l; + } + + if (empty($this->locales)) { + /** @var GettextTranslator $translator */ + $translator = StaticTranslator::$instance; + $this->locales = $translator->listLocales(); + } + } + + protected function callTranslated($callback, $arguments, $locale = 'en_US') + { + /** @var GettextTranslator $translator */ + $translator = StaticTranslator::$instance; + $translator->setLocale($locale); + return call_user_func_array($callback, $arguments); + } + + protected function getMultiTranslated($name, $callback, $arguments, $locales = null) + { + if ($locales === null) { + $locales = $this->locales; + } + array_unshift($locales, 'C'); + + $rows = array(); + + foreach ($arguments as $k => $args) { + $row = array($name => $k); + + if (! is_array($args)) { + $args = array($args); + } + foreach ($locales as $locale) { + $row[$locale] = $this->callTranslated($callback, $args, $locale); + } + $rows[] = $row; + } + + return $rows; + } + + protected function printTable($rows) + { + $tt = new ArrayToTextTableHelper($rows); + $tt->showHeaders(true); + $tt->render(); + echo "\n\n"; + } +} diff --git a/modules/translation/doc/01-About.md b/modules/translation/doc/01-About.md new file mode 100644 index 0000000..2eaacfa --- /dev/null +++ b/modules/translation/doc/01-About.md @@ -0,0 +1,6 @@ +# About the Translation Module <a id="translation-module-about"></a> + +Please read the following chapters for more insights on this module: + +* [Installation](02-Installation.md#translation-module-installation) +* [Translations](03-Translation.md#module-translation-introduction) diff --git a/modules/translation/doc/02-Installation.md b/modules/translation/doc/02-Installation.md new file mode 100644 index 0000000..04f85c8 --- /dev/null +++ b/modules/translation/doc/02-Installation.md @@ -0,0 +1,15 @@ +# Translation Module Installation <a id="translation-module-installation"></a> + +This module is provided with the Icinga Web 2 package and does +not need any extra installation step. + +## Enable the Module <a id="translation-module-enable"></a> + +Navigate to `Configuration` -> `Modules` -> `translation` and enable +the module. + +You can also enable the module during the setup wizard, or on the CLI: + +``` +icingacli module enable translation +``` diff --git a/modules/translation/doc/03-Translation.md b/modules/translation/doc/03-Translation.md new file mode 100644 index 0000000..14e2e88 --- /dev/null +++ b/modules/translation/doc/03-Translation.md @@ -0,0 +1,204 @@ +# Introduction <a id="module-translation-introduction"></a> + +Icinga Web 2 provides localization out of the box - for itself and the core modules. +This module is for third party module developers to aid them to localize their work. + +The chapters [Translation for Developers](03-Translation.md#module-translation-developers), +[Translation for Translators](03-Translation.md#module-translation-translators) and +[Testing Translations](03-Translation.md#module-translation-tests) will introduce and +explain you, how to take part on localizing modules to different languages. + +## Translation for Developers <a id="module-translation-developers"></a> + +To make use of the built-in translations in your module's code or views, you should use the method +`$this->translate('String to be translated')`, let's have a look at an example: + +```php +<?php + +class ExampleController extends Controller +{ + public function indexAction() + { + $this->view->title = $this->translate('Hello World'); + } +} +``` + +So if there a translation available for the `Hello World` string you will get an translated output, depends on the +language which is set in your configuration as the default language, if it is `de_DE` the output would be +`Hallo Welt`. + +The same works also for views: + +``` +<h1><?= $this->title ?></h1> +<p> + <?= $this->translate('Hello World') ?> + <?= $this->translate('String to be translated') ?> +</p> +``` + +If you need to provide placeholders in your messages, you should wrap the `$this->translate()` with `sprintf()` for e.g. + sprintf($this->translate('Hello User: (%s)'), $user->getName()) + +### Translating plural forms <a id="module-translation-plural-forms"></a> + +To provide a plural translation, just use the `translatePlural()` function. + +```php +<?php + +class ExampleController extends Controller +{ + public function indexAction() + { + $this->view->message = $this->translatePlural('Service', 'Services', 3); + } +} +``` + +### Context based translation <a id="module-translation-context-based"></a> + +If you want to provide context based translations, you can easily do it with an extra parameter in both methods +`translate()` and `translatePlural()`. + +```php +<?php + +class ExampleController extends Controller +{ + public function indexAction() + { + $this->view->title = $this->translate('My Title', 'mycontext'); + $this->view->message = $this->translatePlural('Service', 'Services', 3, 'mycontext'); + } +} +``` + +## Translation for Translators <a id="module-translation-translators"></a> + +> **Note**: +> +> If you want to translate Icinga Web 2 or any module made by Icinga, please head over to +> [translate.icinga.com](https://translate.icinga.com) instead. We won't accept any contributions +> in this regard other than those made there. + +Icinga Web 2 internally uses the UNIX standard gettext tool to perform internationalization, this means translation +files in the .po file format are supplied for text strings used in the code. + +There are a lot of tools and techniques to work with .po localization files, you can choose what ever you prefer. We +won't let you alone on your first steps and therefore we'll introduce you a nice tool, called Poedit. + +### Poedit <a id="module-translation-translators-poedit"></a> + +First of all, you have to download and install [Poedit](http://poedit.net). +When you are done, you have to configure Poedit. + +#### Configuration <a id="module-translation-translators-poedit-configuration"></a> + +`Personalize`: Please provide your Name and E-Mail under Identity. + +![Personalize](img/poedit_001.png) + +`Editor`: Under the `Behavior` the Automatically compile .mo files on save, should be disabled. + +![Editor](img/poedit_002.png) + +`Translations Memory`: Under the `Database` please add your languages, for which are you writing translations. + +![Translations Memory](img/poedit_003.png) + +When you are done, just save your new settings. + +#### Editing .po files <a id="module-translation-translators-poedit-edit-po-files"></a> + +> **Note** +> +> ll_CC stands for ll=language and CC=country code for e.g `de_DE`, `fr_FR`, `ru_RU`, `it_IT` etc. + +To work with .po files, open or create the one for your language located under +`application/locale/ll_CC/LC_MESSAGES/yourmodule.po`. As shown below, you will +get then a full list of all available translation strings for the module. Each +module names its translation files `%module_name%.po`. + +![Full list of strings](img/poedit_004.png) + +Now you can make changes and when there is no translation available, Poedit would mark it with a blue color, as shown +below. + +![Untranslated strings](img/poedit_005.png) + +And when you want to test your changes, please read more about under the chapter +[Testing Translations](Testing Translations). + +## Testing Translations <a id="module-translation-tests"></a> + +If you want to try out your translation changes in Icinga Web 2, you can make use of the CLI translations commands. + +> **Note**: +> +> Please make sure that the gettext package is installed + +To get an easier development with translations, you can activate the `translation module` which provides CLI commands, +after that you would be able to refresh and compile your .po files. + +Let's assume, we want to provide German translations for our just new created module `yourmodule`. + +If we haven't yet any translations strings in our .po file or even the .po file, we can use the CLI command, to do the +job for us: + +``` +icingacli translation refresh module yourmodule de_DE +``` + +This will go through all .php and .phtml files inside the module and a look after `$this->translate()` if there is +something to translate - if there is something and is not available in the `yourmodule.po` it will update this file +for us with new strings. + +Now you can open the `application/locale/de_DE/LC_MESSAGES/yourmodule.po` and you will see something similar: + +``` +# Icinga Web 2 - Head for multiple monitoring backends. +# Copyright (C) 2014 Icinga Development Team +# This file is distributed under the same license as Development Module. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Development Module (0.0.1)\n" +"Report-Msgid-Bugs-To: dev@icinga.com\n" +"POT-Creation-Date: 2014-09-09 10:12+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language: ll_CC\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: /modules/yourmodule/configuration.php:6 +msgid "yourmodule" +msgstr "" +``` + +Great, now you can adjust the file and provide the German `msgstr` for `yourmodule`. + +``` +#: /modules/yourmodule/configuration.php:6 +msgid "Dummy" +msgstr "Attrappe" +``` + +The last step is to compile the `yourmodule.po` to the `yourmodule.mo`: + +``` +icingacli translation compile module yourmodule de_DE +``` + +> **Note** +> +> After compiling it you need to restart the web server to get new translations available in your module. + +At this moment, everywhere in the module where the `Dummy` should be translated, it would return the translated +string `Attrappe`. diff --git a/modules/translation/doc/img/poedit_001.png b/modules/translation/doc/img/poedit_001.png Binary files differnew file mode 100644 index 0000000..2d07b8e --- /dev/null +++ b/modules/translation/doc/img/poedit_001.png diff --git a/modules/translation/doc/img/poedit_002.png b/modules/translation/doc/img/poedit_002.png Binary files differnew file mode 100644 index 0000000..d31e5ba --- /dev/null +++ b/modules/translation/doc/img/poedit_002.png diff --git a/modules/translation/doc/img/poedit_003.png b/modules/translation/doc/img/poedit_003.png Binary files differnew file mode 100644 index 0000000..5f285f9 --- /dev/null +++ b/modules/translation/doc/img/poedit_003.png diff --git a/modules/translation/doc/img/poedit_004.png b/modules/translation/doc/img/poedit_004.png Binary files differnew file mode 100644 index 0000000..2c85dd9 --- /dev/null +++ b/modules/translation/doc/img/poedit_004.png diff --git a/modules/translation/doc/img/poedit_005.png b/modules/translation/doc/img/poedit_005.png Binary files differnew file mode 100644 index 0000000..3ae59ba --- /dev/null +++ b/modules/translation/doc/img/poedit_005.png diff --git a/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php new file mode 100644 index 0000000..af01d5f --- /dev/null +++ b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php @@ -0,0 +1,232 @@ +<?php + +namespace Icinga\Module\Translation\Cli; + +/** + * Array to Text Table Generation Class + * + * @author Tony Landis <tony@tonylandis.com> + * @link http://www.tonylandis.com/ + * @copyright Copyright (C) 2006-2009 Tony Landis + * @license http://www.opensource.org/licenses/bsd-license.php + */ +class ArrayToTextTableHelper +{ + /** + * @var array The array for processing + */ + protected $rows; + + /** + * @var int The column width settings + */ + protected $cs = array(); + + /** + * @var int The Row lines settings + */ + protected $rs = array(); + + /** + * @var int The Column index of keys + */ + protected $keys = array(); + + /** + * @var int Max Column Height (returns) + */ + protected $mH = 2; + + /** + * @var int Max Row Width (chars) + */ + protected $mW = 30; + + protected $head = false; + protected $pcen = "+"; + protected $prow = "-"; + protected $pcol = "|"; + + + /** + * Prepare array into textual format + * + * @param array $rows The input array + * @param bool $head Show heading + * @param int $maxWidth Max Column Height (returns) + * @param int $maxHeight Max Row Width (chars) + */ + public function __construct($rows) + { + $this->rows =& $rows; + $this->cs = array(); + $this->rs = array(); + + if (! $xc = count($this->rows)) { + return false; + } + + $this->keys = array_keys($this->rows[0]); + $columns = count($this->keys); + + for ($x = 0; $x < $xc; $x++) { + for ($y = 0; $y < $columns; $y++) { + $this->setMax($x, $y, $this->rows[$x][$this->keys[$y]]); + } + } + + return $this; + } + + /** + * Show the headers using the key values of the array for the titles + * + * @param bool $bool + */ + public function showHeaders($bool) + { + if ($bool) { + $this->setHeading(); + } + } + + /** + * Set the maximum width (number of characters) per column before truncating + * + * @param int $maxWidth + */ + public function setMaxWidth($maxWidth) + { + $this->mW = (int) $maxWidth; + } + + /** + * Set the maximum height (number of lines) per row before truncating + * + * @param int $maxHeight + */ + public function setMaxHeight($maxHeight) + { + $this->mH = (int) $maxHeight; + } + + /** + * Prints the data to a text table + * + * @param bool $return Set to 'true' to return text rather than printing + * + * @return mixed + */ + public function render($return = false) + { + if ($return) { + ob_start(null, 0, true); + } + + $this->printLine(); + $this->printHeading(); + + $rc = count($this->rows); + for ($i = 0; $i < $rc; $i++) { + $this->printRow($i); + } + + $this->printLine(false); + + if ($return) { + $contents = ob_get_contents(); + ob_end_clean(); + return $contents; + } + return null; + } + + protected function setHeading() + { + $data = array(); + foreach ($this->keys as $colKey => $value) { + $this->setMax(false, $colKey, $value); + $data[$colKey] = strtoupper($value); + } + if (! is_array($data)) { + return false; + } + $this->head = $data; + + return $this; + } + + protected function printLine($nl = true) + { + print $this->pcen; + foreach ($this->cs as $key => $val) { + print $this->prow . + str_pad('', $val, $this->prow, STR_PAD_RIGHT) . + $this->prow . + $this->pcen; + } + if ($nl) { + print "\n"; + } + } + + protected function printHeading() + { + if (! is_array($this->head)) { + return false; + } + + print $this->pcol; + foreach ($this->cs as $key => $val) { + print ' ' . + str_pad($this->head[$key], $val, ' ', STR_PAD_BOTH) . + ' ' . + $this->pcol; + } + + print "\n"; + $this->printLine(); + + return $this; + } + + protected function printRow($rowKey) + { + // loop through each line + for ($line = 1; $line <= $this->rs[$rowKey]; $line++) { + print $this->pcol; + for ($colKey = 0; $colKey < count($this->keys); $colKey++) { + print " "; + print str_pad( + substr($this->rows[$rowKey][$this->keys[$colKey]], ($this->mW * ($line - 1)), $this->mW), + $this->cs[$colKey], + ' ', + STR_PAD_RIGHT + ); + print " " . $this->pcol; + } + print "\n"; + } + } + + protected function setMax($rowKey, $colKey, &$colVal) + { + $w = mb_strlen($colVal); + $h = 1; + if ($w > $this->mW) { + $h = ceil($w % $this->mW); + if ($h > $this->mH) { + $h = $this->mH; + } + $w = $this->mW; + } + + if (! isset($this->cs[$colKey]) || $this->cs[$colKey] < $w) { + $this->cs[$colKey] = $w; + } + + if ($rowKey !== false && (! isset($this->rs[$rowKey]) || $this->rs[$rowKey] < $h)) { + $this->rs[$rowKey] = $h; + } + } +} diff --git a/modules/translation/library/Translation/Cli/TranslationCommand.php b/modules/translation/library/Translation/Cli/TranslationCommand.php new file mode 100644 index 0000000..af3582c --- /dev/null +++ b/modules/translation/library/Translation/Cli/TranslationCommand.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Cli; + +use Exception; +use Icinga\Cli\Command; +use Icinga\Exception\IcingaException; +use Icinga\Module\Translation\Util\GettextTranslationHelper; + +/** + * Base class for translation commands + */ +class TranslationCommand extends Command +{ + /** + * Get the gettext translation helper + * + * @param string $locale + * + * @return GettextTranslationHelper + */ + public function getTranslationHelper($locale) + { + $helper = new GettextTranslationHelper($this->app, $locale); + $helper->setConfig($this->Config()); + return $helper; + } + + /** + * Check whether the given locale code is valid + * + * @param string $code The locale code to validate + * + * @return string The validated locale code + * + * @throws Exception In case the locale code is invalid + */ + public function validateLocaleCode($code) + { + if (! preg_match('@[a-z]{2}_[A-Z]{2}@', $code)) { + throw new IcingaException( + 'Locale code \'%s\' is not valid. Expected format is: ll_CC', + $code + ); + } + + return $code; + } + + /** + * Check whether the given module is available and enabled + * + * @param string $name The module name to validate + * + * @return string The validated module name + * + * @throws Exception In case the given module is not available or not enabled + */ + public function validateModuleName($name) + { + $enabledModules = $this->app->getModuleManager()->listEnabledModules(); + + if (! in_array($name, $enabledModules)) { + throw new IcingaException( + 'Module with name \'%s\' not found or is not enabled', + $name + ); + } + + return $name; + } +} diff --git a/modules/translation/library/Translation/Util/GettextTranslationHelper.php b/modules/translation/library/Translation/Util/GettextTranslationHelper.php new file mode 100644 index 0000000..d1e6ac2 --- /dev/null +++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php @@ -0,0 +1,442 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Translation\Util; + +use Exception; +use Icinga\Application\ApplicationBootstrap; +use Icinga\Application\Config; +use Icinga\Application\Modules\Manager; +use Icinga\Exception\IcingaException; +use Icinga\Util\File; + +/** + * This class provides some useful utility functions to handle gettext translations + */ +class GettextTranslationHelper +{ + /** + * All project files are supposed to have the same/this encoding + */ + const FILE_ENCODING = 'UTF-8'; + + /** + * Config + * + * @var Config + */ + protected $config; + + /** + * The source files to parse + * + * @var array + */ + private $sourceExtensions = array( + 'php', + 'phtml' + ); + + /** + * The module manager of the application's bootstrap + * + * @var Manager + */ + private $moduleMgr; + + /** + * The current version of Icingaweb 2 or of the module the catalog is being created for + * + * @var string + */ + private $version; + + /** + * The name of the module if any + * + * @var string + */ + private $moduleName; + + /** + * The locale used by this helper + * + * @var string + */ + private $locale; + + /** + * The path to the module, if any + * + * @var string + */ + private $moduleDir; + + /** + * The path to the file catalog + * + * @var string + */ + private $catalogPath; + + /** + * The path to the *.pot file + * + * @var string + */ + private $templatePath; + + /** + * The path to the *.po file + * + * @var string + */ + private $tablePath; + + /** + * Create a new TranslationHelper object + * + * @param ApplicationBootstrap $bootstrap The application's bootstrap object + * @param string $locale The locale to be used by this helper + */ + public function __construct(ApplicationBootstrap $bootstrap, $locale) + { + $this->moduleMgr = $bootstrap->getModuleManager(); + $this->locale = $locale; + } + + /** + * Cleanup temporary files + */ + public function __destruct() + { + if ($this->catalogPath !== null && file_exists($this->catalogPath)) { + unlink($this->catalogPath); + } + + if ($this->templatePath !== null && file_exists($this->templatePath)) { + unlink($this->templatePath); + } + } + + /** + * Get the config + * + * @return Config + */ + public function getConfig() + { + return $this->config; + } + + /** + * Set the config + * + * @param Config $config + * + * @return $this + */ + public function setConfig(Config $config) + { + $this->config = $config; + return $this; + } + + /** + * Update the translation table for a particular module + * + * @param string $module The name of the module for which to update the translation table + */ + public function updateModuleTranslations($module) + { + $this->catalogPath = tempnam(sys_get_temp_dir(), 'IcingaTranslation_'); + $this->templatePath = tempnam(sys_get_temp_dir(), 'IcingaPot_'); + $this->version = $this->moduleMgr->getModule($module)->getVersion(); + $this->moduleName = $this->moduleMgr->getModule($module)->getName(); + + $this->moduleDir = $this->moduleMgr->getModuleDir($module); + $this->tablePath = implode( + DIRECTORY_SEPARATOR, + array( + $this->moduleDir, + 'application', + 'locale', + $this->locale, + 'LC_MESSAGES', + $module . '.po' + ) + ); + + $this->createFileCatalog(); + $this->createTemplateFile(); + $this->updateTranslationTable(); + } + + /** + * Compile the translation table for a particular module + * + * @param string $module The name of the module for which to compile the translation table + */ + public function compileModuleTranslation($module) + { + $this->moduleDir = $this->moduleMgr->getModuleDir($module); + $this->tablePath = implode( + DIRECTORY_SEPARATOR, + array( + $this->moduleDir, + 'application', + 'locale', + $this->locale, + 'LC_MESSAGES', + $module . '.po' + ) + ); + + $this->compileTranslationTable(); + } + + /** + * Update any existing or create a new translation table using the gettext tools + * + * @throws Exception In case the translation table does not yet exist and cannot be created + */ + private function updateTranslationTable() + { + if (is_file($this->tablePath)) { + shell_exec(sprintf( + '%s --update --backup=none %s %s 2>&1', + $this->getConfig()->get('translation', 'msgmerge', '/usr/bin/env msgmerge'), + $this->tablePath, + $this->templatePath + )); + } else { + if ((!is_dir(dirname($this->tablePath)) && !@mkdir(dirname($this->tablePath), 0755, true)) || + !rename($this->templatePath, $this->tablePath)) { + throw new IcingaException( + 'Unable to create %s', + $this->tablePath + ); + } + } + $this->updateHeader($this->tablePath); + $this->fixSourceLocations($this->tablePath); + } + + /** + * Create the template file using the gettext tools + */ + private function createTemplateFile() + { + shell_exec( + implode( + ' ', + array( + $this->getConfig()->get('translation', 'xgettext', '/usr/bin/env xgettext'), + '--language=PHP', + '--keyword=translate', + '--keyword=translate:1,2c', + '--keyword=translateInDomain:2', + '--keyword=translateInDomain:2,3c', + '--keyword=translatePlural:1,2', + '--keyword=translatePlural:1,2,4c', + '--keyword=translatePluralInDomain:2,3', + '--keyword=translatePluralInDomain:2,3,5c', + '--keyword=mt:2', + '--keyword=mt:2,3c', + '--keyword=mtp:2,3', + '--keyword=mtp:2,3,5c', + '--keyword=t', + '--keyword=t:1,2c', + '--keyword=tp:1,2', + '--keyword=tp:1,2,4c', + '--keyword=N_', + '--sort-output', + '--force-po', + '--omit-header', + '--from-code=' . self::FILE_ENCODING, + '--files-from="' . $this->catalogPath . '"', + '--output="' . $this->templatePath . '"' + ) + ) + ); + } + + /** + * Create or update a gettext conformant header in the given file + * + * @param string $path The path to the file + */ + private function updateHeader($path) + { + $headerInfo = array( + 'title' => $this->moduleMgr->getModule($this->moduleName)->getTitle(), + 'copyright_holder' => 'TEAM NAME', + 'copyright_year' => date('Y'), + 'author_name' => 'FIRST AUTHOR', + 'author_mail' => 'EMAIL@ADDRESS', + 'author_year' => 'YEAR', + 'project_name' => ucfirst($this->moduleName) . ' Module', + 'project_version' => $this->version, + 'project_bug_mail' => 'ISSUE TRACKER', + 'pot_creation_date' => date('Y-m-d H:iO'), + 'po_revision_date' => 'YEAR-MO-DA HO:MI+ZONE', + 'translator_name' => 'FULL NAME', + 'translator_mail' => 'EMAIL@ADDRESS', + 'language' => $this->locale, + 'language_team_name' => 'LANGUAGE', + 'language_team_url' => 'LL@li.org', + 'charset' => self::FILE_ENCODING + ); + + $content = file_get_contents($path); + if (strpos($content, '# ') === 0) { + $authorInfo = array(); + if (preg_match('@# (.+) <(.+)>, (\d+|YEAR)\.@', $content, $authorInfo)) { + $headerInfo['author_name'] = $authorInfo[1]; + $headerInfo['author_mail'] = $authorInfo[2]; + $headerInfo['author_year'] = $authorInfo[3]; + } + $revisionInfo = array(); + if (preg_match('@Revision-Date: (\d{4}-\d{2}-\d{2} \d{2}:\d{2}\+\d{4})@', $content, $revisionInfo)) { + $headerInfo['po_revision_date'] = $revisionInfo[1]; + } + $translatorInfo = array(); + if (preg_match('@Last-Translator: (.+) <(.+)>@', $content, $translatorInfo)) { + $headerInfo['translator_name'] = $translatorInfo[1]; + $headerInfo['translator_mail'] = $translatorInfo[2]; + } + $languageTeamInfo = array(); + if (preg_match('@Language-Team: (.+) <(.+)>@', $content, $languageTeamInfo)) { + $headerInfo['language_team_name'] = $languageTeamInfo[1]; + $headerInfo['language_team_url'] = $languageTeamInfo[2]; + } + $languageInfo = array(); + if (preg_match('@Language: ([a-z]{2}_[A-Z]{2})@', $content, $languageInfo)) { + $headerInfo['language'] = $languageInfo[1]; + } + } + + file_put_contents( + $path, + implode( + PHP_EOL, + array( + '# ' . $headerInfo['title'] . '.', + '# Copyright (C) ' . $headerInfo['copyright_year'] . ' ' . $headerInfo['copyright_holder'], + '# This file is distributed under the same license as ' . $headerInfo['project_name'] . '.', + '# ' . $headerInfo['author_name'] . ' <' . $headerInfo['author_mail'] + . '>, ' . $headerInfo['author_year'] . '.', + '# ', + '#, fuzzy', + 'msgid ""', + 'msgstr ""', + '"Project-Id-Version: ' . $headerInfo['project_name'] . ' (' + . $headerInfo['project_version'] . ')\n"', + '"Report-Msgid-Bugs-To: ' . $headerInfo['project_bug_mail'] . '\n"', + '"POT-Creation-Date: ' . $headerInfo['pot_creation_date'] . '\n"', + '"PO-Revision-Date: ' . $headerInfo['po_revision_date'] . '\n"', + '"Last-Translator: ' . $headerInfo['translator_name'] . ' <' + . $headerInfo['translator_mail'] . '>\n"', + '"Language: ' . $headerInfo['language'] . '\n"', + '"Language-Team: ' . $headerInfo['language_team_name'] . ' <' + . $headerInfo['language_team_url'] . '>\n"', + '"MIME-Version: 1.0\n"', + '"Content-Type: text/plain; charset=' . $headerInfo['charset'] . '\n"', + '"Content-Transfer-Encoding: 8bit\n"', + '"Plural-Forms: nplurals=2; plural=(n != 1);\n"', + '"X-Poedit-Basepath: .\n"', + '"X-Poedit-SearchPath-0: .\n"', + '' + ) + ) . PHP_EOL . substr($content, strpos($content, '#: ')) + ); + } + + /** + * Adjust all absolute source file paths so that they're all relative to the catalog's location + * + * @param string $path + */ + protected function fixSourceLocations($path) + { + shell_exec(sprintf( + "sed -i 's;%s;../../../..;g' %s", + $this->moduleDir, + $path + )); + } + + /** + * Create the file catalog + * + * @throws Exception In case the catalog-file cannot be created + */ + private function createFileCatalog() + { + $catalog = new File($this->catalogPath, 'w'); + + try { + $this->getSourceFileNames($this->moduleDir, $catalog); + } catch (Exception $error) { + throw $error; + } + + $catalog->fflush(); + } + + /** + * Recursively scan the given directory for translatable source files + * + * @param string $directory The directory where to search for sources + * @param File $file The file where to write the results + * @param array $blacklist A list of directories to omit + * + * @throws Exception In case the given directory is not readable + */ + private function getSourceFileNames($directory, File $file) + { + $directoryHandle = opendir($directory); + if (!$directoryHandle) { + throw new IcingaException( + 'Unable to read files from %s', + $directory + ); + } + + $subdirs = array(); + while (($filename = readdir($directoryHandle)) !== false) { + if ($filename[0] === '.' || $filename === 'vendor') { + continue; + } + $filepath = $directory . DIRECTORY_SEPARATOR . $filename; + if (preg_match('@^[^\.].+\.(' . implode('|', $this->sourceExtensions) . ')$@', $filename)) { + $file->fwrite($filepath . PHP_EOL); + } elseif (! is_link($filepath) && is_dir($filepath)) { + $subdirs[] = $filepath; + } + } + closedir($directoryHandle); + + foreach ($subdirs as $subdir) { + $this->getSourceFileNames($subdir, $file); + } + } + + /** + * Compile the translation table + */ + private function compileTranslationTable() + { + $targetPath = substr($this->tablePath, 0, strrpos($this->tablePath, '.')) . '.mo'; + shell_exec( + implode( + ' ', + array( + $this->getConfig()->get('translation', 'msgfmt', '/usr/bin/env msgfmt'), + '-o ' . $targetPath, + $this->tablePath + ) + ) + ); + } +} diff --git a/modules/translation/module.info b/modules/translation/module.info new file mode 100644 index 0000000..57a0dd2 --- /dev/null +++ b/modules/translation/module.info @@ -0,0 +1,7 @@ +Module: translation +Version: 2.11.4 +Description: Translation module + This module allows developers and translators to translate modules for multiple + languages. You do not need this module to run an internationalized web frontend. + This is only for people who want to contribute translations or translate just + their own modules. |