diff options
Diffstat (limited to 'application/controllers')
-rw-r--r-- | application/controllers/HostController.php | 66 | ||||
-rw-r--r-- | application/controllers/IndexController.php | 20 | ||||
-rw-r--r-- | application/controllers/NodeController.php | 148 | ||||
-rw-r--r-- | application/controllers/ProcessController.php | 780 | ||||
-rw-r--r-- | application/controllers/ServiceController.php | 74 | ||||
-rw-r--r-- | application/controllers/SuggestionsController.php | 372 |
6 files changed, 1460 insertions, 0 deletions
diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php new file mode 100644 index 0000000..e22edde --- /dev/null +++ b/application/controllers/HostController.php @@ -0,0 +1,66 @@ +<?php + +namespace Icinga\Module\Businessprocess\Controllers; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Businessprocess\IcingaDbObject; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\DataView\DataView; +use Icinga\Web\Url; +use ipl\Stdlib\Filter; + +class HostController extends Controller +{ + /** + * True if business process prefers to use icingadb as backend for it's nodes + * + * @var bool + */ + protected $isIcingadbPreferred; + + protected function moduleInit() + { + $this->isIcingadbPreferred = Module::exists('icingadb') + && ! $this->params->has('backend') + && IcingadbSupport::useIcingaDbAsBackend(); + + if (! $this->isIcingadbPreferred) { + parent::moduleInit(); + } + } + + public function showAction() + { + if ($this->isIcingadbPreferred) { + $hostName = $this->params->shift('host'); + + $query = Host::on(IcingaDbObject::fetchDb()); + IcingaDbObject::applyIcingaDbRestrictions($query); + + $query->filter(Filter::equal('host.name', $hostName)); + + $host = $query->first(); + + $this->params->add('name', $hostName); + + if ($host !== null) { + $this->redirectNow(Url::fromPath('icingadb/host')->setParams($this->params)); + } + } else { + $hostName = $this->params->get('host'); + + $query = $this->backend->select() + ->from('hoststatus', array('host_name')) + ->where('host_name', $hostName); + + $this->applyRestriction('monitoring/filter/objects', $query); + if ($query->fetchRow() !== false) { + $this->redirectNow(Url::fromPath('monitoring/host/show')->setParams($this->params)); + } + } + + $this->view->host = $hostName; + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..60ddc70 --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Businessprocess\Controllers; + +use Icinga\Module\Businessprocess\Web\Controller; +use Icinga\Module\Businessprocess\Web\Component\Dashboard; + +class IndexController extends Controller +{ + /** + * Show an overview page + */ + public function indexAction() + { + $this->setTitle($this->translate('Business Process Overview')); + $this->controls()->add($this->overviewTab()); + $this->content()->add(Dashboard::create($this->Auth(), $this->storage())); + $this->setAutorefreshInterval(15); + } +} diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php new file mode 100644 index 0000000..e5c657f --- /dev/null +++ b/application/controllers/NodeController.php @@ -0,0 +1,148 @@ +<?php + +namespace Icinga\Module\Businessprocess\Controllers; + +use Exception; +use Icinga\Application\Modules\Module; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Businessprocess\Renderer\Breadcrumb; +use Icinga\Module\Businessprocess\Renderer\TileRenderer; +use Icinga\Module\Businessprocess\Simulation; +use Icinga\Module\Businessprocess\State\IcingaDbState; +use Icinga\Module\Businessprocess\State\MonitoringState; +use Icinga\Module\Businessprocess\Web\Controller; +use Icinga\Module\Businessprocess\Web\Url; +use ipl\Html\Html; +use ipl\Web\Widget\Link; + +class NodeController extends Controller +{ + public function impactAction() + { + $this->setAutorefreshInterval(10); + $content = $this->content(); + $this->controls()->add( + $this->singleTab($this->translate('Node Impact')) + ); + $name = $this->params->get('name'); + $this->addTitle($this->translate('Business Impact (%s)'), $name); + + $brokenFiles = []; + $simulation = Simulation::fromSession($this->session()); + foreach ($this->storage()->listProcessNames() as $configName) { + try { + $config = $this->storage()->loadProcess($configName); + } catch (Exception $e) { + $meta = $this->storage()->loadMetadata($configName); + $brokenFiles[$meta->get('Title')] = $configName; + continue; + } + + $parents = []; + if ($config->hasNode($name)) { + foreach ($config->getNode($name)->getPaths() as $path) { + array_pop($path); // Remove the monitored node + $immediateParentName = array_pop($path); // The directly affected process + $parents[] = [$config->getNode($immediateParentName), $path]; + } + } + + $askedConfigs = []; + foreach ($config->getImportedNodes() as $importedNode) { + $importedConfig = $importedNode->getBpConfig(); + + if (isset($askedConfigs[$importedConfig->getName()])) { + continue; + } else { + $askedConfigs[$importedConfig->getName()] = true; + } + + if ($importedConfig->hasNode($name)) { + $node = $importedConfig->getNode($name); + $nativePaths = $node->getPaths($config); + + do { + $path = array_pop($nativePaths); + $importedNodePos = array_search($importedNode->getIdentifier(), $path, true); + if ($importedNodePos !== false) { + array_pop($path); // Remove the monitored node + $immediateParentName = array_pop($path); // The directly affected process + $importedPath = array_slice($path, $importedNodePos + 1); + + // We may get multiple native paths. Though, only the right hand of the path + // is what we're interested in. The left part is not what is getting imported. + $antiDuplicator = join('|', $importedPath) . '|' . $immediateParentName; + if (isset($parents[$antiDuplicator])) { + continue; + } + + foreach ($importedNode->getPaths($config) as $targetPath) { + if ($targetPath[count($targetPath) - 1] === $immediateParentName) { + array_pop($targetPath); + $parent = $importedNode; + } else { + $parent = $importedConfig->getNode($immediateParentName); + } + + $parents[$antiDuplicator] = [$parent, array_merge($targetPath, $importedPath)]; + } + } + } while (! empty($nativePaths)); + } + } + + if (empty($parents)) { + continue; + } + + if (Module::exists('icingadb') && + (! $config->getBackendName() && IcingadbSupport::useIcingaDbAsBackend()) + ) { + IcingaDbState::apply($config); + } else { + MonitoringState::apply($config); + } + $config->applySimulation($simulation); + + foreach ($parents as $parentAndPath) { + $renderer = (new TileRenderer($config, array_shift($parentAndPath))) + ->setUrl(Url::fromPath('businessprocess/process/show', ['config' => $configName])) + ->setPath(array_shift($parentAndPath)); + + $bc = Breadcrumb::create($renderer); + $bc->getAttributes()->set('data-base-target', '_next'); + $content->add($bc); + } + } + + if ($content->isEmpty()) { + $content->add($this->translate('No impact detected. Is this node part of a business process?')); + } + + if (! empty($brokenFiles)) { + $elem = Html::tag( + 'ul', + ['class' => 'broken-files'], + tp( + 'The following business process has an invalid config file and therefore cannot be read:', + 'The following business processes have invalid config files and therefore cannot be read:', + count($brokenFiles) + ) + ); + + foreach ($brokenFiles as $bpName => $fileName) { + $elem->addHtml( + Html::tag( + 'li', + new Link( + sprintf('%s (%s.conf)', $bpName, $fileName), + \ipl\Web\Url::fromPath('businessprocess/process/show', ['config' => $fileName]) + ) + ) + ); + } + + $content->addHtml($elem); + } + } +} diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php new file mode 100644 index 0000000..208c91e --- /dev/null +++ b/application/controllers/ProcessController.php @@ -0,0 +1,780 @@ +<?php + +namespace Icinga\Module\Businessprocess\Controllers; + +use Icinga\Application\Modules\Module; +use Icinga\Date\DateFormatter; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Forms\AddNodeForm; +use Icinga\Module\Businessprocess\Forms\EditNodeForm; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Businessprocess\Renderer\Breadcrumb; +use Icinga\Module\Businessprocess\Renderer\Renderer; +use Icinga\Module\Businessprocess\Renderer\TileRenderer; +use Icinga\Module\Businessprocess\Renderer\TreeRenderer; +use Icinga\Module\Businessprocess\Simulation; +use Icinga\Module\Businessprocess\State\IcingaDbState; +use Icinga\Module\Businessprocess\State\MonitoringState; +use Icinga\Module\Businessprocess\Storage\ConfigDiff; +use Icinga\Module\Businessprocess\Storage\LegacyConfigRenderer; +use Icinga\Module\Businessprocess\Web\Component\ActionBar; +use Icinga\Module\Businessprocess\Web\Component\RenderedProcessActionBar; +use Icinga\Module\Businessprocess\Web\Component\Tabs; +use Icinga\Module\Businessprocess\Web\Controller; +use Icinga\Util\Json; +use Icinga\Web\Notification; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\OutputFormat; +use ipl\Html\Form; +use ipl\Html\Html; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\TemplateString; +use ipl\Html\Text; +use ipl\Web\Control\SortControl; +use ipl\Web\FormElement\TermInput; +use ipl\Web\Widget\Link; +use ipl\Web\Widget\Icon; + +class ProcessController extends Controller +{ + /** @var Renderer */ + protected $renderer; + + /** + * Create a new Business Process Configuration + */ + public function createAction() + { + $this->assertPermission('businessprocess/create'); + + $title = $this->translate('Create a new Business Process'); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForCreate()->activate('create')) + ->add(Html::tag('h1', null, $title)); + + $this->content()->add( + $this->loadForm('bpConfig') + ->setStorage($this->storage()) + ->setSuccessUrl('businessprocess/process/show') + ->handleRequest() + ); + } + + /** + * Upload an existing Business Process Configuration + */ + public function uploadAction() + { + $this->assertPermission('businessprocess/create'); + + $title = $this->translate('Upload a Business Process Config file'); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForCreate()->activate('upload')) + ->add(Html::tag('h1', null, $title)); + + $this->content()->add( + $this->loadForm('BpUpload') + ->setStorage($this->storage()) + ->setSuccessUrl('businessprocess/process/show') + ->handleRequest() + ); + } + + /** + * Show a business process + */ + public function showAction() + { + $bp = $this->loadModifiedBpConfig(); + $node = $this->getNode($bp); + + if (Module::exists('icingadb') && + (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend()) + ) { + IcingaDbState::apply($bp); + } else { + MonitoringState::apply($bp); + } + + $this->handleSimulations($bp); + + $this->setTitle($this->translate('Business Process "%s"'), $bp->getTitle()); + + $renderer = $this->prepareRenderer($bp, $node); + + if (! $this->showFullscreen && ($node === null || ! $renderer->rendersImportedNode())) { + if ($this->params->get('unlocked')) { + $renderer->unlock(); + } + + if ($bp->isEmpty() && $renderer->isLocked()) { + $this->redirectNow($this->url()->with('unlocked', true)); + } + } + + $this->handleFormatRequest($bp, $node); + + $this->prepareControls($bp, $renderer); + + $this->tabs()->extend(new OutputFormat()); + + $this->content()->add($this->showHints($bp, $renderer)); + $this->content()->add($this->showWarnings($bp)); + $this->content()->add($this->showErrors($bp)); + $this->content()->add($renderer); + $this->loadActionForm($bp, $node); + $this->setDynamicAutorefresh(); + } + + /** + * Create a sort control and apply its sort specification to the given renderer + * + * @param Renderer $renderer + * @param BpConfig $config + * + * @return SortControl + */ + protected function createBpSortControl(Renderer $renderer, BpConfig $config): SortControl + { + $defaultSort = $this->session()->get('sort.default', $renderer->getDefaultSort()); + $options = [ + 'display_name asc' => $this->translate('Name'), + 'state desc' => $this->translate('State') + ]; + if ($config->getMetadata()->isManuallyOrdered()) { + $options['manual asc'] = $this->translate('Manual'); + } elseif ($defaultSort === 'manual desc') { + $defaultSort = $renderer->getDefaultSort(); + } + + $sortControl = SortControl::create($options) + ->setDefault($defaultSort) + ->setMethod('POST') + ->setAttribute('name', 'bp-sort-control') + ->on(Form::ON_SUCCESS, function (SortControl $sortControl) use ($renderer) { + $sort = $sortControl->getSort(); + if ($sort === $renderer->getDefaultSort()) { + $this->session()->delete('sort.default'); + $url = Url::fromRequest()->without($sortControl->getSortParam()); + } else { + $this->session()->set('sort.default', $sort); + $url = Url::fromRequest()->with($sortControl->getSortParam(), $sort); + } + + $this->redirectNow($url); + })->handleRequest($this->getServerRequest()); + + $renderer->setSort($sortControl->getSort()); + $this->params->shift($sortControl->getSortParam()); + + return $sortControl; + } + + protected function prepareControls($bp, $renderer) + { + $controls = $this->controls(); + + if ($this->showFullscreen) { + $controls->getAttributes()->add('class', 'want-fullscreen'); + $controls->add(Html::tag( + 'a', + [ + 'href' => $this->url()->without('showFullscreen')->without('view'), + 'title' => $this->translate('Leave full screen and switch back to normal mode') + ], + new Icon('down-left-and-up-right-to-center') + )); + } + + if (! ($this->showFullscreen || $this->view->compact)) { + $controls->add($this->getProcessTabs($bp, $renderer)); + $controls->getAttributes()->add('class', 'separated'); + } + + $controls->add(Breadcrumb::create(clone $renderer)); + if (! $this->showFullscreen && ! $this->view->compact) { + $controls->add( + new RenderedProcessActionBar($bp, $renderer, $this->url()) + ); + } + + $controls->addHtml($this->createBpSortControl($renderer, $bp)); + } + + protected function getNode(BpConfig $bp) + { + if ($nodeName = $this->params->get('node')) { + return $bp->getNode($nodeName); + } else { + return null; + } + } + + protected function prepareRenderer($bp, $node) + { + if ($this->renderer === null) { + if ($this->params->get('mode') === 'tree') { + $renderer = new TreeRenderer($bp, $node); + } else { + $renderer = new TileRenderer($bp, $node); + } + $renderer->setUrl($this->url()) + ->setPath($this->params->getValues('path')); + + $this->renderer = $renderer; + } + + return $this->renderer; + } + + protected function getProcessTabs(BpConfig $bp, Renderer $renderer) + { + $tabs = $this->singleTab($bp->getTitle()); + if ($renderer->isLocked()) { + $tabs->extend(new DashboardAction()); + } + + return $tabs; + } + + protected function handleSimulations(BpConfig $bp) + { + $simulation = Simulation::fromSession($this->session()); + + if ($this->params->get('dismissSimulations')) { + Notification::success( + sprintf( + $this->translate('%d applied simulation(s) have been dropped'), + $simulation->count() + ) + ); + $simulation->clear(); + $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked')); + } + + $bp->applySimulation($simulation); + } + + protected function loadActionForm(BpConfig $bp, Node $node = null) + { + $action = $this->params->get('action'); + $form = null; + if ($this->showFullscreen) { + return; + } + + $canEdit = $bp->getMetadata()->canModify(); + + if ($action === 'add' && $canEdit) { + $form = (new AddNodeForm()) + ->setProcess($bp) + ->setParentNode($node) + ->setStorage($this->storage()) + ->setSession($this->session()) + ->on(AddNodeForm::ON_SUCCESS, function () { + $this->redirectNow(Url::fromRequest()->without('action')); + }) + ->handleRequest($this->getServerRequest()); + + if ($form->hasElement('children')) { + /** @var TermInput $childrenElement */ + $childrenElement = $form->getElement('children'); + foreach ($childrenElement->prepareMultipartUpdate($this->getServerRequest()) as $update) { + if (! is_array($update)) { + $update = [$update]; + } + + $this->addPart(...$update); + } + } + } elseif ($action === 'cleanup' && $canEdit) { + $form = $this->loadForm('CleanupNode') + ->setSuccessUrl(Url::fromRequest()->without('action')) + ->setProcess($bp) + ->setSession($this->session()) + ->handleRequest(); + } elseif ($action === 'editmonitored' && $canEdit) { + $form = (new EditNodeForm()) + ->setProcess($bp) + ->setNode($bp->getNode($this->params->get('editmonitorednode'))) + ->setParentNode($node) + ->setSession($this->session()) + ->on(EditNodeForm::ON_SUCCESS, function () { + $this->redirectNow(Url::fromRequest()->without(['action', 'editmonitorednode'])); + }) + ->handleRequest($this->getServerRequest()); + } elseif ($action === 'delete' && $canEdit) { + $form = $this->loadForm('DeleteNode') + ->setSuccessUrl(Url::fromRequest()->without('action')) + ->setProcess($bp) + ->setNode($bp->getNode($this->params->get('deletenode'))) + ->setParentNode($node) + ->setSession($this->session()) + ->handleRequest(); + } elseif ($action === 'edit' && $canEdit) { + $form = $this->loadForm('Process') + ->setSuccessUrl(Url::fromRequest()->without('action')) + ->setProcess($bp) + ->setNode($bp->getNode($this->params->get('editnode'))) + ->setSession($this->session()) + ->handleRequest(); + } elseif ($action === 'simulation') { + $form = $this->loadForm('simulation') + ->setSuccessUrl(Url::fromRequest()->without('action')) + ->setNode($bp->getNode($this->params->get('simulationnode'))) + ->setSimulation(Simulation::fromSession($this->session())) + ->handleRequest(); + } elseif ($action === 'move') { + $successUrl = $this->url()->without(['action', 'movenode']); + if ($this->params->get('mode') === 'tree') { + // If the user moves a node from a subtree, the `node` param exists + $successUrl->getParams()->remove('node'); + } + + if ($this->session()->get('sort.default')) { + // If there's a default sort specification in the session, it can only be `display_name desc`, + // as otherwise the user wouldn't be able to trigger this action. So it's safe to just define + // descending manual order now. + $successUrl->getParams()->add(SortControl::DEFAULT_SORT_PARAM, 'manual desc'); + } + + $form = $this->loadForm('MoveNode') + ->setSuccessUrl($successUrl) + ->setProcess($bp) + ->setParentNode($node) + ->setSession($this->session()) + ->setNode($bp->getNode($this->params->get('movenode'))) + ->handleRequest(); + } + + if ($form) { + $this->content()->prepend(HtmlString::create((string) $form)); + } + } + + protected function setDynamicAutorefresh() + { + if (! $this->isXhr()) { + // This will trigger the very first XHR refresh immediately on page + // load. Please not that this may hammer the server in case we would + // decide to use autorefreshInterval for HTML meta-refreshes also. + $this->setAutorefreshInterval(1); + return; + } + + if ($this->params->has('action')) { + if ($this->params->get('action') !== 'add') { + // The new add form uses the term input, which doesn't support value persistence across refreshes + $this->setAutorefreshInterval(45); + } + } else { + $this->setAutorefreshInterval(10); + } + } + + protected function showWarnings(BpConfig $bp) + { + if ($bp->hasWarnings()) { + $ul = Html::tag('ul', array('class' => 'warning')); + foreach ($bp->getWarnings() as $warning) { + $ul->add(Html::tag('li')->setContent($warning)); + } + + return $ul; + } else { + return null; + } + } + + protected function showErrors(BpConfig $bp) + { + if ($bp->hasWarnings()) { + $ul = Html::tag('ul', array('class' => 'error')); + foreach ($bp->getErrors() as $msg) { + $ul->add(Html::tag('li')->setContent($msg)); + } + + return $ul; + } else { + return null; + } + } + + protected function showHints(BpConfig $bp, Renderer $renderer) + { + $ul = Html::tag('ul', ['class' => 'error']); + $this->prepareMissingNodeLinks($ul); + foreach ($bp->getErrors() as $error) { + $ul->addHtml(Html::tag('li', $error)); + } + + if ($bp->hasChanges()) { + $li = Html::tag('li')->setSeparator(' '); + $li->add(sprintf( + $this->translate('This process has %d pending change(s).'), + $bp->countChanges() + ))->add(Html::tag( + 'a', + [ + 'href' => Url::fromPath('businessprocess/process/config') + ->setParams($this->getRequest()->getUrl()->getParams()) + ], + $this->translate('Store') + ))->add(Html::tag( + 'a', + ['href' => $this->url()->with('dismissChanges', true)], + $this->translate('Dismiss') + )); + $ul->add($li); + } + + if ($bp->hasSimulations()) { + $li = Html::tag('li')->setSeparator(' '); + $li->add(sprintf( + $this->translate('This process shows %d simulated state(s).'), + $bp->countSimulations() + ))->add(Html::tag( + 'a', + ['href' => $this->url()->with('dismissSimulations', true)], + $this->translate('Dismiss') + )); + $ul->add($li); + } + + if (! $renderer->isLocked() && $renderer->appliesCustomSorting()) { + $ul->addHtml(Html::tag('li', null, [ + Text::create($this->translate('Drag&Drop disabled. Custom sort order applied.')), + (new Form()) + ->setAttribute('class', 'inline') + ->addElement('submitButton', SortControl::DEFAULT_SORT_PARAM, [ + 'label' => $this->translate('Reset to default'), + 'value' => $renderer->getDefaultSort(), + 'class' => 'link-button' + ]) + ->addElement('hidden', 'uid', ['value' => 'bp-sort-control']) + ])->setSeparator(' ')); + } + + if (! $ul->isEmpty()) { + return $ul; + } else { + return null; + } + } + + protected function prepareMissingNodeLinks(HtmlElement $ul): void + { + $missing = array_keys($this->bp->getMissingChildren()); + if (! empty($missing)) { + $missingLinkedNodes = null; + foreach ($this->bp->getImportedNodes() as $process) { + if ($process->hasMissingChildren()) { + $missingLinkedNodes = array_keys($process->getMissingChildren()); + $link = Url::fromPath('businessprocess/process/show') + ->addParams(['config' => $process->getConfigName()]); + + $ul->addHtml(Html::tag( + 'li', + [ + TemplateString::create( + tp( + 'Linked node %s has one missing child node: {{#link}}Show{{/link}}', + 'Linked node %s has %d missing child nodes: {{#link}}Show{{/link}}', + count($missingLinkedNodes) + ), + $process->getAlias(), + count($missingLinkedNodes), + ['link' => new Link(null, (string) $link)] + ) + ] + )); + } + } + + if (! empty($missingLinkedNodes)) { + return; + } + + $count = count($missing); + if ($count > 10) { + $missing = array_slice($missing, 0, 10); + $missing[] = '...'; + } + + $link = Url::fromPath('businessprocess/process/show') + ->addParams(['config' => $this->bp->getName(), 'action' => 'cleanup']); + + $ul->addHtml(Html::tag( + 'li', + [ + TemplateString::create( + tp( + '{{#link}}Cleanup{{/link}} one missing node: %2$s', + '{{#link}}Cleanup{{/link}} %d missing nodes: %s', + count($missing) + ), + ['link' => new Link(null, (string) $link)], + $count, + implode(', ', $missing) + ) + ] + )); + } + } + + /** + * Show the source code for a process + */ + public function sourceAction() + { + $this->assertPermission('businessprocess/modify'); + + $bp = $this->loadModifiedBpConfig(); + $this->view->showDiff = $showDiff = (bool) $this->params->get('showDiff', false); + + $this->view->source = LegacyConfigRenderer::renderConfig($bp); + if ($this->view->showDiff) { + $this->view->diff = ConfigDiff::create( + $this->storage()->getSource($this->view->configName), + $this->view->source + ); + $title = sprintf( + $this->translate('%s: Source Code Differences'), + $bp->getTitle() + ); + } else { + $title = sprintf( + $this->translate('%s: Source Code'), + $bp->getTitle() + ); + } + + $this->setTitle($title); + $this->controls() + ->add($this->tabsForConfig($bp)->activate('source')) + ->add(Html::tag('h1', null, $title)) + ->add($this->createConfigActionBar($bp, $showDiff)); + + $this->setViewScript('process/source'); + } + + /** + * Download a process configuration file + */ + public function downloadAction() + { + $this->assertPermission('businessprocess/modify'); + + $config = $this->loadModifiedBpConfig(); + $response = $this->getResponse(); + $response->setHeader( + 'Content-Disposition', + sprintf( + 'attachment; filename="%s.conf";', + $config->getName() + ) + ); + $response->setHeader('Content-Type', 'text/plain'); + + echo LegacyConfigRenderer::renderConfig($config); + $this->doNotRender(); + } + + /** + * Modify a business process configuration + */ + public function configAction() + { + $this->assertPermission('businessprocess/modify'); + + $bp = $this->loadModifiedBpConfig(); + + $title = sprintf( + $this->translate('%s: Configuration'), + $bp->getTitle() + ); + $this->setTitle($title); + $this->controls() + ->add($this->tabsForConfig($bp)->activate('config')) + ->add(Html::tag('h1', null, $title)) + ->add($this->createConfigActionBar($bp)); + + $url = Url::fromPath('businessprocess/process/show') + ->setParams($this->getRequest()->getUrl()->getParams()); + $this->content()->add( + $this->loadForm('bpConfig') + ->setProcess($bp) + ->setStorage($this->storage()) + ->setSuccessUrl($url) + ->handleRequest() + ); + } + + protected function createConfigActionBar(BpConfig $config, $showDiff = false) + { + $actionBar = new ActionBar(); + + if ($showDiff) { + $params = array('config' => $config->getName()); + $actionBar->add(Html::tag( + 'a', + [ + 'href' => Url::fromPath('businessprocess/process/source', $params), + 'title' => $this->translate('Show source code') + ], + [ + new Icon('file-lines'), + $this->translate('Source'), + ] + )); + } else { + $params = array( + 'config' => $config->getName(), + 'showDiff' => true + ); + + $actionBar->add(Html::tag( + 'a', + [ + 'href' => Url::fromPath('businessprocess/process/source', $params), + 'title' => $this->translate('Highlight changes') + ], + [ + new Icon('shuffle'), + $this->translate('Diff') + ] + )); + } + + $actionBar->add(Html::tag( + 'a', + [ + 'href' => Url::fromPath('businessprocess/process/download', ['config' => $config->getName()]), + 'target' => '_blank', + 'title' => $this->translate('Download process configuration') + ], + [ + new Icon('download'), + $this->translate('Download') + ] + )); + + return $actionBar; + } + + protected function tabsForShow() + { + return $this->tabs()->add('show', array( + 'label' => $this->translate('Business Process'), + 'url' => $this->url() + )); + } + + /** + * @return Tabs + */ + protected function tabsForCreate() + { + return $this->tabs()->add('create', array( + 'label' => $this->translate('Create'), + 'url' => 'businessprocess/process/create' + ))->add('upload', array( + 'label' => $this->translate('Upload'), + 'url' => 'businessprocess/process/upload' + )); + } + + protected function tabsForConfig(BpConfig $config) + { + $params = array( + 'config' => $config->getName() + ); + + $tabs = $this->tabs()->add('config', array( + 'label' => $this->translate('Process Configuration'), + 'url' =>Url::fromPath('businessprocess/process/config', $params) + )); + + if ($this->params->get('showDiff')) { + $params['showDiff'] = true; + } + + $tabs->add('source', array( + 'label' => $this->translate('Source'), + 'url' =>Url::fromPath('businessprocess/process/source', $params) + )); + + return $tabs; + } + + protected function handleFormatRequest(BpConfig $bp, BpNode $node = null) + { + $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')); + } + + switch ($desiredFormat) { + 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($node !== null ? $node->toArray() : $bp->toArray())) + ->sendResponse(); + exit; + case 'csv': + $csv = fopen('php://temp', 'w'); + + fputcsv($csv, ['Path', 'Name', 'State', 'Since', 'In_Downtime']); + + foreach ($node !== null ? $node->toArray(null, true) : $bp->toArray(true) as $node) { + $data = [$node['path'], $node['name']]; + + if (isset($node['state'])) { + $data[] = $node['state']; + } + + if (isset($node['since'])) { + $data[] = DateFormatter::formatDateTime($node['since']); + } + + if (isset($node['in_downtime'])) { + $data[] = $node['in_downtime']; + } + + fputcsv($csv, $data); + } + + $response = $this->getResponse(); + $response + ->setHeader('Content-Type', 'text/csv') + ->setHeader('Cache-Control', 'no-store') + ->setHeader( + 'Content-Disposition', + 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv' + ) + ->sendHeaders(); + + rewind($csv); + + fpassthru($csv); + + exit; + } + } +} diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php new file mode 100644 index 0000000..671d00c --- /dev/null +++ b/application/controllers/ServiceController.php @@ -0,0 +1,74 @@ +<?php + +namespace Icinga\Module\Businessprocess\Controllers; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Businessprocess\IcingaDbObject; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Monitoring\Controller; +use Icinga\Module\Monitoring\DataView\DataView; +use Icinga\Web\Url; +use ipl\Stdlib\Filter; + +class ServiceController extends Controller +{ + /** + * True if business process prefers to use icingadb as backend for it's nodes + * + * @var bool + */ + protected $isIcingadbPreferred; + + protected function moduleInit() + { + $this->isIcingadbPreferred = Module::exists('icingadb') + && ! $this->params->has('backend') + && IcingadbSupport::useIcingaDbAsBackend(); + + if (! $this->isIcingadbPreferred) { + parent::moduleInit(); + } + } + + public function showAction() + { + if ($this->isIcingadbPreferred) { + $hostName = $this->params->shift('host'); + $serviceName = $this->params->shift('service'); + + $query = Service::on(IcingaDbObject::fetchDb()); + IcingaDbObject::applyIcingaDbRestrictions($query); + + $query->filter(Filter::all( + Filter::equal('service.name', $serviceName), + Filter::equal('host.name', $hostName) + )); + + $service = $query->first(); + + $this->params->add('name', $serviceName); + $this->params->add('host.name', $hostName); + + if ($service !== null) { + $this->redirectNow(Url::fromPath('icingadb/service')->setParams($this->params)); + } + } else { + $hostName = $this->params->get('host'); + $serviceName = $this->params->get('service'); + + $query = $this->backend->select() + ->from('servicestatus', array('service_description')) + ->where('host_name', $hostName) + ->where('service_description', $serviceName); + + $this->applyRestriction('monitoring/filter/objects', $query); + if ($query->fetchRow() !== false) { + $this->redirectNow(Url::fromPath('monitoring/service/show')->setParams($this->params)); + } + } + + $this->view->host = $hostName; + $this->view->service = $serviceName; + } +} diff --git a/application/controllers/SuggestionsController.php b/application/controllers/SuggestionsController.php new file mode 100644 index 0000000..9fa0331 --- /dev/null +++ b/application/controllers/SuggestionsController.php @@ -0,0 +1,372 @@ +<?php + +namespace Icinga\Module\Businessprocess\Controllers; + +use Exception; +use Icinga\Data\Filter\Filter as LegacyFilter; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\HostNode; +use Icinga\Module\Businessprocess\IcingaDbObject; +use Icinga\Module\Businessprocess\ImportedNode; +use Icinga\Module\Businessprocess\Monitoring\DataView\HostStatus; +use Icinga\Module\Businessprocess\Monitoring\DataView\ServiceStatus; +use Icinga\Module\Businessprocess\MonitoringRestrictions; +use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Module\Businessprocess\Web\Controller; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Stdlib\Filter; +use ipl\Web\FormElement\TermInput\TermSuggestions; + +class SuggestionsController extends Controller +{ + public function processAction() + { + $ignoreList = []; + $forConfig = null; + $forParent = null; + if ($this->params->has('config')) { + $forConfig = $this->loadModifiedBpConfig(); + + $parentName = $this->params->get('node'); + if ($parentName) { + $forParent = $forConfig->getBpNode($parentName); + + $collectParents = function ($node) use ($ignoreList, &$collectParents) { + foreach ($node->getParents() as $parent) { + $ignoreList[$parent->getName()] = true; + + if ($parent->hasParents()) { + $collectParents($parent); + } + } + }; + + $ignoreList[$parentName] = true; + if ($forParent->hasParents()) { + $collectParents($forParent); + } + + foreach ($forParent->getChildNames() as $name) { + $ignoreList[$name] = true; + } + } + } + + $suggestions = new TermSuggestions((function () use ($forConfig, $forParent, $ignoreList, &$suggestions) { + foreach ($this->storage()->listProcessNames() as $config) { + $differentConfig = false; + if ($forConfig === null || $config !== $forConfig->getName()) { + if ($forConfig !== null && $forParent === null) { + continue; + } + + try { + $bp = $this->storage()->loadProcess($config); + } catch (Exception $_) { + continue; + } + + $differentConfig = true; + } else { + $bp = $forConfig; + } + + foreach ($bp->getBpNodes() as $bpNode) { + /** @var BpNode $bpNode */ + if ($bpNode instanceof ImportedNode) { + continue; + } + + $search = $bpNode->getName(); + if ($differentConfig) { + $search = "@$config:$search"; + } + + if (in_array($search, $suggestions->getExcludeTerms(), true) + || isset($ignoreList[$search]) + || ($forParent + ? $forParent->hasChild($search) + : ($forConfig && $forConfig->hasRootNode($search)) + ) + ) { + continue; + } + + if ($suggestions->matchSearch($bpNode->getName()) + || (! $bpNode->hasAlias() || $suggestions->matchSearch($bpNode->getAlias())) + || $bpNode->getName() === $suggestions->getOriginalSearchValue() + || $bpNode->getAlias() === $suggestions->getOriginalSearchValue() + ) { + yield [ + 'search' => $search, + 'label' => $bpNode->getAlias() ?? $bpNode->getName(), + 'config' => $config + ]; + } + } + } + })()); + $suggestions->setGroupingCallback(function (array $data) { + return $this->storage()->loadMetadata($data['config'])->getTitle(); + }); + + $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); + } + + public function icingadbHostAction() + { + $excludes = Filter::none(); + $forConfig = null; + if ($this->params->has('config')) { + $forConfig = $this->loadModifiedBpConfig(); + + if ($this->params->has('node')) { + $nodeName = $this->params->get('node'); + $node = $forConfig->getBpNode($nodeName); + + foreach ($node->getChildren() as $child) { + if ($child instanceof HostNode) { + $excludes->add(Filter::equal('host.name', $child->getHostname())); + } + } + } + } + + $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) { + foreach ($suggestions->getExcludeTerms() as $excludeTerm) { + [$hostName, $_] = BpConfig::splitNodeName($excludeTerm); + $excludes->add(Filter::equal('host.name', $hostName)); + } + + $hosts = Host::on($forConfig->getBackend()) + ->columns(['host.name', 'host.display_name']) + ->limit(50); + IcingaDbObject::applyIcingaDbRestrictions($hosts); + $hosts->filter(Filter::all( + $excludes, + Filter::any( + Filter::like('host.name', $suggestions->getSearchTerm()), + Filter::equal('host.name', $suggestions->getOriginalSearchValue()), + Filter::like('host.display_name', $suggestions->getSearchTerm()), + Filter::equal('host.display_name', $suggestions->getOriginalSearchValue()), + Filter::like('host.address', $suggestions->getSearchTerm()), + Filter::equal('host.address', $suggestions->getOriginalSearchValue()), + Filter::like('host.address6', $suggestions->getSearchTerm()), + Filter::equal('host.address6', $suggestions->getOriginalSearchValue()), + Filter::like('host.customvar_flat.flatvalue', $suggestions->getSearchTerm()), + Filter::equal('host.customvar_flat.flatvalue', $suggestions->getOriginalSearchValue()), + Filter::like('hostgroup.name', $suggestions->getSearchTerm()), + Filter::equal('hostgroup.name', $suggestions->getOriginalSearchValue()) + ) + )); + foreach ($hosts as $host) { + yield [ + 'search' => BpConfig::joinNodeName($host->name, 'Hoststatus'), + 'label' => $host->display_name, + 'class' => 'host' + ]; + } + })()); + + $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); + } + + public function icingadbServiceAction() + { + $excludes = Filter::none(); + $forConfig = null; + if ($this->params->has('config')) { + $forConfig = $this->loadModifiedBpConfig(); + + if ($this->params->has('node')) { + $nodeName = $this->params->get('node'); + $node = $forConfig->getBpNode($nodeName); + + foreach ($node->getChildren() as $child) { + if ($child instanceof ServiceNode) { + $excludes->add(Filter::all( + Filter::equal('host.name', $child->getHostname()), + Filter::equal('service.name', $child->getServiceDescription()) + )); + } + } + } + } + + $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) { + foreach ($suggestions->getExcludeTerms() as $excludeTerm) { + [$hostName, $serviceName] = BpConfig::splitNodeName($excludeTerm); + if ($serviceName !== null && $serviceName !== 'Hoststatus') { + $excludes->add(Filter::all( + Filter::equal('host.name', $hostName), + Filter::equal('service.name', $serviceName) + )); + } + } + + $services = Service::on($forConfig->getBackend()) + ->columns(['host.name', 'host.display_name', 'service.name', 'service.display_name']) + ->limit(50); + IcingaDbObject::applyIcingaDbRestrictions($services); + $services->filter(Filter::all( + $excludes, + Filter::any( + Filter::like('host.name', $suggestions->getSearchTerm()), + Filter::equal('host.name', $suggestions->getOriginalSearchValue()), + Filter::like('host.display_name', $suggestions->getSearchTerm()), + Filter::equal('host.display_name', $suggestions->getOriginalSearchValue()), + Filter::like('service.name', $suggestions->getSearchTerm()), + Filter::equal('service.name', $suggestions->getOriginalSearchValue()), + Filter::like('service.display_name', $suggestions->getSearchTerm()), + Filter::equal('service.display_name', $suggestions->getOriginalSearchValue()), + Filter::like('service.customvar_flat.flatvalue', $suggestions->getSearchTerm()), + Filter::equal('service.customvar_flat.flatvalue', $suggestions->getOriginalSearchValue()), + Filter::like('servicegroup.name', $suggestions->getSearchTerm()), + Filter::equal('servicegroup.name', $suggestions->getOriginalSearchValue()) + ) + )); + foreach ($services as $service) { + yield [ + 'class' => 'service', + 'search' => BpConfig::joinNodeName($service->host->name, $service->name), + 'label' => sprintf( + $this->translate('%s on %s', '<service> on <host>'), + $service->display_name, + $service->host->display_name + ) + ]; + } + })()); + + $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); + } + + public function monitoringHostAction() + { + $excludes = LegacyFilter::matchAny(); + $forConfig = null; + if ($this->params->has('config')) { + $forConfig = $this->loadModifiedBpConfig(); + + if ($this->params->has('node')) { + $nodeName = $this->params->get('node'); + $node = $forConfig->getBpNode($nodeName); + + foreach ($node->getChildren() as $child) { + if ($child instanceof HostNode) { + $excludes->addFilter(LegacyFilter::where('host_name', $child->getHostname())); + } + } + } + } + + $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) { + foreach ($suggestions->getExcludeTerms() as $excludeTerm) { + [$hostName, $_] = BpConfig::splitNodeName($excludeTerm); + $excludes->addFilter(LegacyFilter::where('host_name', $hostName)); + } + + $hosts = (new HostStatus($forConfig->getBackend()->select(), ['host_name', 'host_display_name'])) + ->limit(50) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->applyFilter(LegacyFilter::matchAny( + LegacyFilter::where('host_name', $suggestions->getSearchTerm()), + LegacyFilter::where('host_display_name', $suggestions->getSearchTerm()), + LegacyFilter::where('host_address', $suggestions->getSearchTerm()), + LegacyFilter::where('host_address6', $suggestions->getSearchTerm()), + LegacyFilter::where('_host_%', $suggestions->getSearchTerm()), + // This also forces a group by on the query, needed anyway due to the custom var filter + // above, which may return multiple rows because of the wildcard in the name filter. + LegacyFilter::where('hostgroup_name', $suggestions->getSearchTerm()), + LegacyFilter::where('hostgroup_alias', $suggestions->getSearchTerm()) + )); + if (! $excludes->isEmpty()) { + $hosts->applyFilter(LegacyFilter::not($excludes)); + } + + foreach ($hosts as $row) { + yield [ + 'search' => BpConfig::joinNodeName($row->host_name, 'Hoststatus'), + 'label' => $row->host_display_name, + 'class' => 'host' + ]; + } + })()); + + $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); + } + + public function monitoringServiceAction() + { + $excludes = LegacyFilter::matchAny(); + $forConfig = null; + if ($this->params->has('config')) { + $forConfig = $this->loadModifiedBpConfig(); + + if ($this->params->has('node')) { + $nodeName = $this->params->get('node'); + $node = $forConfig->getBpNode($nodeName); + + foreach ($node->getChildren() as $child) { + if ($child instanceof ServiceNode) { + $excludes->addFilter(LegacyFilter::matchAll( + LegacyFilter::where('host_name', $child->getHostname()), + LegacyFilter::where('service_description', $child->getServiceDescription()) + )); + } + } + } + } + + $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) { + foreach ($suggestions->getExcludeTerms() as $excludeTerm) { + [$hostName, $serviceName] = BpConfig::splitNodeName($excludeTerm); + if ($serviceName !== null && $serviceName !== 'Hoststatus') { + $excludes->addFilter(LegacyFilter::matchAll( + LegacyFilter::where('host_name', $hostName), + LegacyFilter::where('service_description', $serviceName) + )); + } + } + + $services = (new ServiceStatus($forConfig->getBackend()->select(), [ + 'host_name', + 'host_display_name', + 'service_description', + 'service_display_name' + ])) + ->limit(50) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->applyFilter(LegacyFilter::matchAny( + LegacyFilter::where('host_name', $suggestions->getSearchTerm()), + LegacyFilter::where('host_display_name', $suggestions->getSearchTerm()), + LegacyFilter::where('service_description', $suggestions->getSearchTerm()), + LegacyFilter::where('service_display_name', $suggestions->getSearchTerm()), + LegacyFilter::where('_service_%', $suggestions->getSearchTerm()), + // This also forces a group by on the query, needed anyway due to the custom var filter + // above, which may return multiple rows because of the wildcard in the name filter. + LegacyFilter::where('servicegroup_name', $suggestions->getSearchTerm()), + LegacyFilter::where('servicegroup_alias', $suggestions->getSearchTerm()) + )); + if (! $excludes->isEmpty()) { + $services->applyFilter(LegacyFilter::not($excludes)); + } + + foreach ($services as $row) { + yield [ + 'class' => 'service', + 'search' => BpConfig::joinNodeName($row->host_name, $row->service_description), + 'label' => sprintf( + $this->translate('%s on %s', '<service> on <host>'), + $row->service_display_name, + $row->host_display_name + ) + ]; + } + })()); + + $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest())); + } +} |