diff options
Diffstat (limited to 'application')
24 files changed, 3742 insertions, 0 deletions
diff --git a/application/clicommands/CheckCommand.php b/application/clicommands/CheckCommand.php new file mode 100644 index 0000000..d1c561f --- /dev/null +++ b/application/clicommands/CheckCommand.php @@ -0,0 +1,23 @@ +<?php + +namespace Icinga\Module\Businessprocess\Clicommands; + +class CheckCommand extends ProcessCommand +{ + public function listActions() + { + return array('process'); + } + + /** + * 'check process' is DEPRECATED, please use 'process check' instead + * + * USAGE + * + * icingacli businessprocess check process [--config <name>] <process> + */ + public function processAction() + { + $this->checkAction(); + } +} diff --git a/application/clicommands/CleanupCommand.php b/application/clicommands/CleanupCommand.php new file mode 100644 index 0000000..f0041c8 --- /dev/null +++ b/application/clicommands/CleanupCommand.php @@ -0,0 +1,106 @@ +<?php + +namespace Icinga\Module\Businessprocess\Clicommands; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Application\Modules\Module; +use Icinga\Cli\Command; +use Icinga\Module\Businessprocess\Modification\NodeRemoveAction; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Businessprocess\State\IcingaDbState; +use Icinga\Module\Businessprocess\State\MonitoringState; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; + +class CleanupCommand extends Command +{ + /** + * @var LegacyStorage + */ + protected $storage; + + protected $defaultActionName = 'cleanup'; + + public function init() + { + $this->storage = LegacyStorage::getInstance(); + } + + /** + * Cleanup all missing monitoring nodes from the specified config name + * If no config name is specified, the missing nodes are cleaned from all available configs. + * Invalid config files and file names are ignored + * + * USAGE + * + * icingacli businessprocess cleanup [<config-name>] + * + * OPTIONS + * + * <config-name> + */ + public function cleanupAction(): void + { + $configNames = (array) $this->params->shift() ?: $this->storage->listAllProcessNames(); + $foundMissingNode = false; + foreach ($configNames as $configName) { + if (! $this->storage->hasProcess($configName)) { + continue; + } + + try { + $bp = $this->storage->loadProcess($configName); + } catch (Exception $e) { + Logger::error( + 'Failed to scan the %s.conf file for missing nodes. Faulty config found.', + $configName + ); + + continue; + } + + if (Module::exists('icingadb') + && (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend()) + ) { + IcingaDbState::apply($bp); + } else { + MonitoringState::apply($bp); + } + + $removedNodes = []; + foreach (array_keys($bp->getMissingChildren()) as $missingNode) { + $node = $bp->getNode($missingNode); + $remove = new NodeRemoveAction($node); + + try { + if ($remove->appliesTo($bp)) { + $remove->applyTo($bp); + $removedNodes[] = $node->getName(); + $this->storage->storeProcess($bp); + $bp->clearAppliedChanges(); + + $foundMissingNode = true; + } + } catch (Exception $e) { + Logger::error(sprintf('(%s.conf) %s', $configName, $e->getMessage())); + + continue; + } + } + + if (! empty($removedNodes)) { + echo sprintf( + 'Removed following %d missing node(s) from %s.conf successfully:', + count($removedNodes), + $configName + ); + + echo "\n" . implode("\n", $removedNodes) . "\n\n"; + } + } + + if (! $foundMissingNode) { + echo "No missing node found.\n"; + } + } +} diff --git a/application/clicommands/ProcessCommand.php b/application/clicommands/ProcessCommand.php new file mode 100644 index 0000000..018c1e3 --- /dev/null +++ b/application/clicommands/ProcessCommand.php @@ -0,0 +1,227 @@ +<?php + +namespace Icinga\Module\Businessprocess\Clicommands; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Application\Modules\Module; +use Icinga\Cli\Command; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\HostNode; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Businessprocess\State\IcingaDbState; +use Icinga\Module\Businessprocess\State\MonitoringState; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; + +class ProcessCommand extends Command +{ + /** + * @var LegacyStorage + */ + protected $storage; + + protected $hostColors = array( + 0 => array('black', 'lightgreen'), + 1 => array('lightgray', 'lightred'), + 2 => array('black', 'brown'), + 99 => array('black', 'lightgray'), + ); + + protected $serviceColors = array( + 0 => array('black', 'lightgreen'), + 1 => array('black', 'yellow'), + 2 => array('lightgray', 'lightred'), + 3 => array('black', 'lightpurple'), + 99 => array('black', 'lightgray'), + ); + + public function init() + { + $this->storage = LegacyStorage::getInstance(); + } + + /** + * List all available Business Process Configurations + * + * ...or their BusinessProcess Nodes in case a Configuration name is given + * + * USAGE + * + * icingacli businessprocess list processes [<config-name>] [options] + * + * OPTIONS + * + * <config-name> + * --no-title Show only names and no related title + */ + public function listAction() + { + if ($config = $this->params->shift()) { + $this->listBpNames($this->storage->loadProcess($config)); + } else { + $this->listConfigNames(! (bool) $this->params->shift('no-title')); + } + } + + protected function listConfigNames($withTitle) + { + foreach ($this->storage->listProcesses() as $key => $title) { + if ($withTitle) { + echo $title . "\n"; + } else { + echo $key . "\n"; + } + } + } + + /** + * Check a specific process + * + * USAGE + * + * icingacli businessprocess process check <process> [options] + * + * OPTIONS + * + * --config <configname> Name of the config that contains <process> + * --details Show problem details as a tree + * --colors Show colored output + * --state-type <type> Define which state type to look at. Could be + * either soft or hard, overrides an eventually + * configured default + * --blame Show problem details as a tree reduced to the + * nodes which have the same state as the business + * process + * --root-cause Used in combination with --blame. Only shows + * the path of the nodes which are responsible for + * the state of the business process + * --downtime-is-ok Treat hosts/services in downtime always as + * UP/OK. + * --ack-is-ok Treat acknowledged hosts/services always as + * UP/OK. + */ + public function checkAction() + { + $nodeName = $this->params->shift(); + if (! $nodeName) { + Logger::error('A process name is required'); + exit(1); + } + + $name = $this->params->get('config'); + try { + if ($name === null) { + $name = $this->getFirstProcessName(); + } + + $bp = $this->storage->loadProcess($name); + } catch (Exception $err) { + Logger::error("Can't access configuration '%s': %s", $name, $err->getMessage()); + + exit(3); + } + + if (null !== ($stateType = $this->params->get('state-type'))) { + if ($stateType === 'soft') { + $bp->useSoftStates(); + } + if ($stateType === 'hard') { + $bp->useHardStates(); + } + } + + try { + /** @var BpNode $node */ + $node = $bp->getNode($nodeName); + if (Module::exists('icingadb') + && (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend()) + ) { + IcingaDbState::apply($bp); + } else { + MonitoringState::apply($bp); + } + + if ($bp->hasErrors()) { + Logger::error("Checking Business Process '%s' failed: %s\n", $name, $bp->getErrors()); + + exit(3); + } + } catch (Exception $err) { + Logger::error("Checking Business Process '%s' failed: %s", $name, $err); + + exit(3); + } + + if ($this->params->shift('ack-is-ok')) { + Node::setAckIsOk(); + } + + if ($this->params->shift('downtime-is-ok')) { + Node::setDowntimeIsOk(); + } + + printf("Business Process %s: %s\n", $node->getStateName(), $node->getAlias()); + if ($this->params->shift('details')) { + echo $this->renderProblemTree($node->getProblemTree(), $this->params->shift('colors')); + } + if ($this->params->shift('blame')) { + echo $this->renderProblemTree( + $node->getProblemTreeBlame($this->params->shift('root-cause')), + $this->params->shift('colors') + ); + } + + exit($node->getState()); + } + + protected function listBpNames(BpConfig $config) + { + foreach ($config->listBpNodes() as $title) { + echo $title . "\n"; + } + } + + protected function renderProblemTree($tree, $useColors = false, $depth = 0, BpNode $parent = null) + { + $output = ''; + + foreach ($tree as $name => $subtree) { + /** @var Node $node */ + $node = $subtree['node']; + $state = $parent !== null ? $parent->getChildState($node) : $node->getState(); + + if ($node instanceof HostNode) { + $colors = $this->hostColors[$state]; + } else { + $colors = $this->serviceColors[$state]; + } + + $state = sprintf('[%s]', $node->getStateName($state)); + if ($useColors) { + $state = $this->screen->colorize($state, $colors[0], $colors[1]); + } + + $output .= sprintf( + "%s%s %s %s\n", + str_repeat(' ', $depth), + $node instanceof BpNode ? $node->getOperator() : '-', + $state, + $node->getAlias() + ); + + if ($node instanceof BpNode) { + $output .= $this->renderProblemTree($subtree['children'], $useColors, $depth + 1, $node); + } + } + + return $output; + } + + protected function getFirstProcessName() + { + $list = $this->storage->listProcessNames(); + return key($list); + } +} 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())); + } +} diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php new file mode 100644 index 0000000..3840d8a --- /dev/null +++ b/application/forms/AddNodeForm.php @@ -0,0 +1,412 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Exception; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Common\Sort; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Storage\Storage; +use Icinga\Module\Businessprocess\Web\Form\Element\IplStateOverrides; +use Icinga\Module\Businessprocess\Web\Form\Validator\HostServiceTermValidator; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Session\SessionNamespace; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; +use ipl\Stdlib\Str; +use ipl\Web\Compat\CompatForm; +use ipl\Web\FormElement\TermInput; +use ipl\Web\Url; + +class AddNodeForm extends CompatForm +{ + use Sort; + use Translation; + + /** @var Storage */ + protected $storage; + + /** @var ?BpConfig */ + protected $bp; + + /** @var ?BpNode */ + protected $parent; + + /** @var SessionNamespace */ + protected $session; + + /** + * Set the storage to use + * + * @param Storage $storage + * + * @return $this + */ + public function setStorage(Storage $storage): self + { + $this->storage = $storage; + + return $this; + } + + /** + * Set the affected configuration + * + * @param BpConfig $bp + * + * @return $this + */ + public function setProcess(BpConfig $bp): self + { + $this->bp = $bp; + + return $this; + } + + /** + * Set the affected sub-process + * + * @param ?BpNode $node + * + * @return $this + */ + public function setParentNode(BpNode $node = null): self + { + $this->parent = $node; + + return $this; + } + + /** + * Set the user's session + * + * @param SessionNamespace $session + * + * @return $this + */ + public function setSession(SessionNamespace $session): self + { + $this->session = $session; + + return $this; + } + + protected function assemble() + { + if ($this->parent !== null) { + $title = sprintf($this->translate('Add a node to %s'), $this->parent->getAlias()); + $nodeTypes = [ + 'host' => $this->translate('Host'), + 'service' => $this->translate('Service'), + 'process' => $this->translate('Existing Process'), + 'new-process' => $this->translate('New Process') + ]; + } else { + $title = $this->translate('Add a new root node'); + if (! $this->bp->isEmpty()) { + $nodeTypes = [ + 'process' => $this->translate('Existing Process'), + 'new-process' => $this->translate('New Process') + ]; + } else { + $nodeTypes = []; + } + } + + $this->addHtml(new HtmlElement('h2', null, Text::create($title))); + + if (! empty($nodeTypes)) { + $this->addElement('select', 'node_type', [ + 'label' => $this->translate('Node type'), + 'options' => array_merge( + ['' => ' - ' . $this->translate('Please choose') . ' - '], + $nodeTypes + ), + 'disabledOptions' => [''], + 'class' => 'autosubmit', + 'required' => true, + 'ignore' => true + ]); + + $nodeType = $this->getPopulatedValue('node_type'); + } else { + $nodeType = 'new-process'; + } + + if ($nodeType === 'new-process') { + $this->assembleNewProcessElements(); + } elseif ($nodeType === 'process') { + $this->assembleExistingProcessElements(); + } elseif ($nodeType === 'host') { + $this->assembleHostElements(); + } elseif ($nodeType === 'service') { + $this->assembleServiceElements(); + } + + $this->addElement('submit', 'submit', [ + 'label' => $this->translate('Add Process') + ]); + } + + protected function assembleNewProcessElements(): void + { + $this->addElement('text', 'name', [ + 'required' => true, + 'ignore' => true, + 'label' => $this->translate('ID'), + 'description' => $this->translate('This is the unique identifier of this process'), + 'validators' => [ + 'callback' => function ($value, $validator) { + if ($this->parent !== null ? $this->parent->hasChild($value) : $this->bp->hasRootNode($value)) { + $validator->addMessage( + sprintf($this->translate('%s is already defined in this process'), $value) + ); + + return false; + } + + return true; + } + ] + ]); + + $this->addElement('text', 'alias', [ + 'label' => $this->translate('Display Name'), + 'description' => $this->translate( + 'Usually this name will be shown for this node. Equals ID if not given' + ), + ]); + + $this->addElement('select', 'operator', [ + 'required' => true, + 'label' => $this->translate('Operator'), + 'multiOptions' => Node::getOperators() + ]); + + $display = 1; + if (! $this->bp->isEmpty() && $this->bp->getMetadata()->isManuallyOrdered()) { + $rootNodes = self::applyManualSorting($this->bp->getRootNodes()); + $display = end($rootNodes)->getDisplay() + 1; + } + $this->addElement('select', 'display', [ + 'required' => true, + 'label' => $this->translate('Visualization'), + 'description' => $this->translate('Where to show this process'), + 'value' => $this->parent !== null ? '0' : "$display", + 'multiOptions' => [ + "$display" => $this->translate('Toplevel Process'), + '0' => $this->translate('Subprocess only'), + ] + ]); + + $this->addElement('text', 'infoUrl', [ + 'label' => $this->translate('Info URL'), + 'description' => $this->translate('URL pointing to more information about this node') + ]); + } + + protected function assembleExistingProcessElements(): void + { + $termValidator = function (array $terms) { + foreach ($terms as $term) { + /** @var TermInput\ValidatedTerm $term */ + $nodeName = $term->getSearchValue(); + if ($nodeName[0] === '@') { + if ($this->parent === null) { + $term->setMessage($this->translate('Imported nodes cannot be used as root nodes')); + } elseif (strpos($nodeName, ':') === false) { + $term->setMessage($this->translate('Missing node name')); + } else { + [$config, $nodeName] = Str::trimSplit(substr($nodeName, 1), ':', 2); + if (! $this->storage->hasProcess($config)) { + $term->setMessage($this->translate('Config does not exist or access has been denied')); + } else { + try { + $bp = $this->storage->loadProcess($config); + } catch (Exception $e) { + $term->setMessage( + sprintf($this->translate('Cannot load config: %s'), $e->getMessage()) + ); + } + + if (isset($bp)) { + if (! $bp->hasNode($nodeName)) { + $term->setMessage($this->translate('No node with this name found in config')); + } else { + $term->setLabel($bp->getNode($nodeName)->getAlias()); + } + } + } + } + } elseif (! $this->bp->hasNode($nodeName)) { + $term->setMessage($this->translate('No node with this name found in config')); + } else { + $term->setLabel($this->bp->getNode($nodeName)->getAlias()); + } + + if ($this->parent !== null && $this->parent->hasChild($term->getSearchValue())) { + $term->setMessage($this->translate('Already defined in this process')); + } + + if ($this->parent !== null && $term->getSearchValue() === $this->parent->getName()) { + $term->setMessage($this->translate('Results in a parent/child loop')); + } + } + }; + + $this->addElement( + (new TermInput('children')) + ->setRequired() + ->setVerticalTermDirection() + ->setLabel($this->translate('Process Nodes')) + ->setSuggestionUrl(Url::fromPath('businessprocess/suggestions/process', [ + 'node' => isset($this->parent) ? $this->parent->getName() : null, + 'config' => $this->bp->getName(), + 'showCompact' => true, + '_disableLayout' => true + ])) + ->on(TermInput::ON_ENRICH, $termValidator) + ->on(TermInput::ON_ADD, $termValidator) + ->on(TermInput::ON_PASTE, $termValidator) + ->on(TermInput::ON_SAVE, $termValidator) + ); + } + + protected function assembleHostElements(): void + { + if ($this->bp->getBackend() instanceof MonitoringBackend) { + $suggestionsPath = 'businessprocess/suggestions/monitoring-host'; + } else { + $suggestionsPath = 'businessprocess/suggestions/icingadb-host'; + } + + $this->addElement($this->createChildrenElementForObjects( + $this->translate('Hosts'), + $suggestionsPath + )); + + $this->addElement('checkbox', 'host_override', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Override Host State') + ]); + if ($this->getPopulatedValue('host_override') === 'y') { + $this->addElement(new IplStateOverrides('stateOverrides', [ + 'label' => $this->translate('State Overrides'), + 'options' => [ + 0 => $this->translate('UP'), + 1 => $this->translate('DOWN'), + 99 => $this->translate('PENDING') + ] + ])); + } + } + + protected function assembleServiceElements(): void + { + if ($this->bp->getBackend() instanceof MonitoringBackend) { + $suggestionsPath = 'businessprocess/suggestions/monitoring-service'; + } else { + $suggestionsPath = 'businessprocess/suggestions/icingadb-service'; + } + + $this->addElement($this->createChildrenElementForObjects( + $this->translate('Services'), + $suggestionsPath + )); + + $this->addElement('checkbox', 'service_override', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Override Service State') + ]); + if ($this->getPopulatedValue('service_override') === 'y') { + $this->addElement(new IplStateOverrides('stateOverrides', [ + 'label' => $this->translate('State Overrides'), + 'options' => [ + 0 => $this->translate('OK'), + 1 => $this->translate('WARNING'), + 2 => $this->translate('CRITICAL'), + 3 => $this->translate('UNKNOWN'), + 99 => $this->translate('PENDING'), + ] + ])); + } + } + + protected function createChildrenElementForObjects(string $label, string $suggestionsPath): TermInput + { + $termValidator = function (array $terms) { + (new HostServiceTermValidator()) + ->setParent($this->parent) + ->isValid($terms); + }; + + return (new TermInput('children')) + ->setRequired() + ->setLabel($label) + ->setVerticalTermDirection() + ->setSuggestionUrl(Url::fromPath($suggestionsPath, [ + 'node' => isset($this->parent) ? $this->parent->getName() : null, + 'config' => $this->bp->getName(), + 'showCompact' => true, + '_disableLayout' => true + ])) + ->on(TermInput::ON_ENRICH, $termValidator) + ->on(TermInput::ON_ADD, $termValidator) + ->on(TermInput::ON_PASTE, $termValidator) + ->on(TermInput::ON_SAVE, $termValidator); + } + + protected function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + + $nodeType = $this->getValue('node_type'); + if (! $nodeType || $nodeType === 'new-process') { + $properties = $this->getValues(); + if (! $properties['alias']) { + unset($properties['alias']); + } + + if ($this->parent !== null) { + $properties['parentName'] = $this->parent->getName(); + } + + $changes->createNode(BpConfig::escapeName($this->getValue('name')), $properties); + } else { + /** @var TermInput $term */ + $term = $this->getElement('children'); + $children = array_unique(array_map(function ($term) { + return $term->getSearchValue(); + }, $term->getTerms())); + + if ($nodeType === 'host' || $nodeType === 'service') { + $stateOverrides = $this->getValue('stateOverrides'); + if (! empty($stateOverrides)) { + $childOverrides = []; + foreach ($children as $nodeName) { + $childOverrides[$nodeName] = $stateOverrides; + } + + $changes->modifyNode($this->parent, [ + 'stateOverrides' => array_merge($this->parent->getStateOverrides(), $childOverrides) + ]); + } + } + + if ($this->parent !== null) { + $changes->addChildrenToNode($children, $this->parent); + } else { + foreach ($children as $nodeName) { + $changes->copyNode($nodeName); + } + } + } + + unset($changes); + } +} diff --git a/application/forms/BpConfigForm.php b/application/forms/BpConfigForm.php new file mode 100644 index 0000000..8a0bc95 --- /dev/null +++ b/application/forms/BpConfigForm.php @@ -0,0 +1,236 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Authentication\Auth; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; + +class BpConfigForm extends BpConfigBaseForm +{ + protected $deleteButtonName; + + public function setup() + { + $this->addElement('text', 'name', array( + 'label' => $this->translate('ID'), + 'required' => true, + 'validators' => array( + array( + 'validator' => 'StringLength', + 'options' => array( + 'min' => 2, + 'max' => 40 + ) + ), + [ + 'validator' => 'Regex', + 'options' => [ + 'pattern' => '/^[a-zA-Z0-9](?:[\w\h._-]*)?\w$/', + 'messages' => [ + 'regexNotMatch' => $this->translate( + 'Id must only consist of alphanumeric characters.' + . ' Underscore at the beginning and space, dot and hyphen at the beginning' + . ' and end are not allowed.' + ) + ] + ] + ] + ), + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + )); + + $this->addElement('text', 'Title', array( + 'label' => $this->translate('Display Name'), + 'description' => $this->translate( + 'Usually this name will be shown for this process. Equals ID' + . ' if not given' + ), + )); + + $this->addElement('textarea', 'Description', array( + 'label' => $this->translate('Description'), + 'description' => $this->translate( + 'A slightly more detailed description for this process, about 100-150 characters long' + ), + 'rows' => 4, + )); + + if (! empty($this->listAvailableBackends())) { + $this->addElement('select', 'Backend', array( + 'label' => $this->translate('Backend'), + 'description' => $this->translate( + 'Icinga Web Monitoring Backend where current object states for' + . ' this process should be retrieved from' + ), + 'multiOptions' => array( + null => $this->translate('Use the configured default backend'), + ) + $this->listAvailableBackends() + )); + } + + $this->addElement('select', 'Statetype', array( + 'label' => $this->translate('State Type'), + 'required' => true, + 'description' => $this->translate( + 'Whether this process should be based on Icinga hard or soft states' + ), + 'multiOptions' => array( + 'soft' => $this->translate('Use SOFT states'), + 'hard' => $this->translate('Use HARD states'), + ) + )); + + $this->addElement('select', 'AddToMenu', array( + 'label' => $this->translate('Add to menu'), + 'required' => true, + 'description' => $this->translate( + 'Whether this process should be linked in the main Icinga Web 2 menu' + ), + 'multiOptions' => array( + 'yes' => $this->translate('Yes'), + 'no' => $this->translate('No'), + ) + )); + + $this->addElement('text', 'AllowedUsers', array( + 'label' => $this->translate('Allowed Users'), + 'description' => $this->translate( + 'Allowed Users (comma-separated)' + ), + )); + + $this->addElement('text', 'AllowedGroups', array( + 'label' => $this->translate('Allowed Groups'), + 'description' => $this->translate( + 'Allowed Groups (comma-separated)' + ), + )); + + $this->addElement('text', 'AllowedRoles', array( + 'label' => $this->translate('Allowed Roles'), + 'description' => $this->translate( + 'Allowed Roles (comma-separated)' + ), + )); + + if ($this->bp === null) { + $this->setSubmitLabel( + $this->translate('Add') + ); + } else { + $config = $this->bp; + + $meta = $config->getMetadata(); + foreach ($meta->getProperties() as $k => $v) { + if ($el = $this->getElement($k)) { + $el->setValue($v); + } + } + $this->getElement('name') + ->setValue($config->getName()) + ->setAttrib('readonly', true); + + $this->setSubmitLabel( + $this->translate('Store') + ); + + $label = $this->translate('Delete'); + $el = $this->createElement('submit', $label, array( + 'data-base-target' => '_main' + ))->setLabel($label)->setDecorators(array('ViewHelper')); + $this->deleteButtonName = $el->getName(); + $this->addElement($el); + } + } + + protected function onSetup() + { + $this->getElement($this->getSubmitLabel())->setAttrib('data-base-target', '_main'); + } + + protected function onRequest() + { + $name = $this->getValue('name'); + + if ($this->shouldBeDeleted()) { + if ($this->bp->isReferenced()) { + $this->addError(sprintf( + $this->translate('Process "%s" cannot be deleted as it has been referenced in other processes'), + $name + )); + } else { + $this->bp->clearAppliedChanges(); + $this->storage->deleteProcess($name); + $this->setSuccessUrl('businessprocess'); + $this->redirectOnSuccess(sprintf('Process %s has been deleted', $name)); + } + } + } + + public function onSuccess() + { + $name = $this->getValue('name'); + + if ($this->bp === null) { + if ($this->storage->hasProcess($name)) { + $this->addError(sprintf( + $this->translate('A process named "%s" already exists'), + $name + )); + + return; + } + + // New config + $config = new BpConfig(); + $config->setName($name); + + if (! $this->prepareMetadata($config)) { + return; + } + + $this->setSuccessUrl( + $this->getSuccessUrl()->setParams( + array('config' => $name, 'unlocked' => true) + ) + ); + $this->setSuccessMessage(sprintf('Process %s has been created', $name)); + } else { + $config = $this->bp; + $this->setSuccessMessage(sprintf('Process %s has been stored', $name)); + } + $meta = $config->getMetadata(); + foreach ($this->getValues() as $key => $value) { + if (! in_array($key, ['Title', 'Description', 'Backend'], true) + && ($value === null || $value === '')) { + continue; + } + + if ($meta->hasKey($key)) { + $meta->set($key, $value); + } + } + + $this->storage->storeProcess($config); + $config->clearAppliedChanges(); + parent::onSuccess(); + } + + public function hasDeleteButton() + { + return $this->deleteButtonName !== null; + } + + public function shouldBeDeleted() + { + if (! $this->hasDeleteButton()) { + return false; + } + + $name = $this->deleteButtonName; + return $this->getSentValue($name) === $this->getElement($name)->getLabel(); + } +} diff --git a/application/forms/BpUploadForm.php b/application/forms/BpUploadForm.php new file mode 100644 index 0000000..a746740 --- /dev/null +++ b/application/forms/BpUploadForm.php @@ -0,0 +1,207 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Exception; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Storage\LegacyConfigParser; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; +use Icinga\Web\Notification; + +class BpUploadForm extends BpConfigBaseForm +{ + protected $node; + + protected $objectList = array(); + + protected $processList = array(); + + protected $deleteButtonName; + + private $sourceCode; + + /** @var BpConfig */ + private $uploadedConfig; + + public function setup() + { + $this->showUpload(); + if ($this->hasSource()) { + $this->showDetails(); + } + } + + protected function showDetails() + { + $this->addElement('text', 'name', array( + 'label' => $this->translate('Name'), + 'required' => true, + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + 'validators' => array( + array( + 'validator' => 'StringLength', + 'options' => array( + 'min' => 2, + 'max' => 40 + ) + ), + [ + 'validator' => 'Regex', + 'options' => [ + 'pattern' => '/^[a-zA-Z0-9](?:[\w\h._-]*)?\w$/', + 'messages' => [ + 'regexNotMatch' => $this->translate( + 'Id must only consist of alphanumeric characters.' + . ' Underscore at the beginning and space, dot and hyphen at the beginning' + . ' and end are not allowed.' + ) + ] + ] + ] + ), + )); + + $this->addElement('textarea', 'source', array( + 'label' => $this->translate('Source'), + 'description' => $this->translate( + 'Business process source code' + ), + 'value' => $this->sourceCode, + 'class' => 'preformatted smaller', + 'rows' => 7, + )); + + $this->getUploadedConfig(); + + $this->setSubmitLabel( + $this->translate('Store') + ); + } + + public function getUploadedConfig() + { + if ($this->uploadedConfig === null) { + $this->uploadedConfig = $this->parseSubmittedSourceCode(); + } + + return $this->uploadedConfig; + } + + protected function parseSubmittedSourceCode() + { + $code = $this->getSentValue('source'); + $name = $this->getSentValue('name', '<new config>'); + if (empty($code)) { + $code = $this->sourceCode; + } + + try { + $config = LegacyConfigParser::parseString($name, $code); + + if ($config->hasErrors()) { + foreach ($config->getErrors() as $error) { + $this->addError($error); + } + } + } catch (Exception $e) { + $this->addError($e->getMessage()); + return null; + } + + return $config; + } + + protected function hasSource() + { + if ($this->hasBeenSent() && $source = $this->getSentValue('source')) { + $this->sourceCode = $source; + } else { + $this->processUploadedSource(); + } + + if (empty($this->sourceCode)) { + return false; + } else { + $this->removeElement('uploaded_file'); + return true; + } + } + + protected function showUpload() + { + $this->setAttrib('enctype', 'multipart/form-data'); + + $this->addElement('file', 'uploaded_file', array( + 'label' => $this->translate('File'), + 'destination' => $this->getTempDir(), + 'required' => true, + )); + + /** @var \Zend_Form_Element_File $el */ + $el = $this->getElement('uploaded_file'); + $el->setValueDisabled(true); + + $this->setSubmitLabel( + $this->translate('Next') + ); + } + + protected function getTempDir() + { + return sys_get_temp_dir(); + } + + protected function processUploadedSource() + { + /** @var ?\Zend_Form_Element_File $el */ + $el = $this->getElement('uploaded_file'); + + if ($el && $this->hasBeenSent()) { + $tmpdir = $this->getTempDir(); + $tmpfile = tempnam($tmpdir, 'bpupload_'); + + // TODO: race condition, try to do this without unlinking here + unlink($tmpfile); + + $el->addFilter('Rename', $tmpfile); + if ($el->receive()) { + $this->sourceCode = file_get_contents($tmpfile); + unlink($tmpfile); + } else { + foreach ($el->file->getMessages() as $error) { + $this->addError($error); + } + } + } + + return $this; + } + + public function onSuccess() + { + $config = $this->getUploadedConfig(); + $name = $config->getName(); + + if ($this->storage->hasProcess($name)) { + $this->addError(sprintf( + $this->translate('A process named "%s" already exists'), + $name + )); + + return; + } + + if (! $this->prepareMetadata($config)) { + return; + } + + $this->storage->storeProcess($config); + Notification::success(sprintf('Process %s has been stored', $name)); + + $this->getSuccessUrl()->setParam('config', $name); + + parent::onSuccess(); + } +} diff --git a/application/forms/CleanupNodeForm.php b/application/forms/CleanupNodeForm.php new file mode 100644 index 0000000..c6e5398 --- /dev/null +++ b/application/forms/CleanupNodeForm.php @@ -0,0 +1,61 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Session\SessionNamespace; +use ipl\Html\Html; +use ipl\Sql\Connection as IcingaDbConnection; + +class CleanupNodeForm extends BpConfigBaseForm +{ + /** @var MonitoringBackend|IcingaDbConnection */ + protected $backend; + + /** @var BpConfig */ + protected $bp; + + /** @var SessionNamespace */ + protected $session; + + public function setup() + { + $this->addHtml(Html::tag('h2', $this->translate('Cleanup missing nodes'))); + + $this->addElement('checkbox', 'cleanup_all', [ + 'class' => 'autosubmit', + 'label' => $this->translate('Cleanup all missing nodes'), + 'description' => $this->translate('Remove all missing nodes from config') + ]); + + if ($this->getSentValue('cleanup_all') !== '1') { + $this->addElement('multiselect', 'nodes', [ + 'label' => $this->translate('Select nodes to cleanup'), + 'required' => true, + 'size' => 8, + 'multiOptions' => $this->bp->getMissingChildren() + ]); + } + } + + public function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + + $nodesToCleanup = $this->getValue('cleanup_all') === '1' + ? array_keys($this->bp->getMissingChildren()) + : $this->getValue('nodes'); + + foreach ($nodesToCleanup as $nodeName) { + $node = $this->bp->getNode($nodeName); + $changes->deleteNode($node); + } + + unset($changes); + + parent::onSuccess(); + } +} diff --git a/application/forms/DeleteNodeForm.php b/application/forms/DeleteNodeForm.php new file mode 100644 index 0000000..dba0710 --- /dev/null +++ b/application/forms/DeleteNodeForm.php @@ -0,0 +1,125 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; +use Icinga\Web\View; + +class DeleteNodeForm extends BpConfigBaseForm +{ + /** @var Node */ + protected $node; + + /** @var ?BpNode */ + protected $parentNode; + + public function setup() + { + $node = $this->node; + $nodeName = $node->getAlias() ?? $node->getName(); + + /** @var View $view */ + $view = $this->getView(); + $this->addHtml( + '<h2>' . $view->escape( + sprintf($this->translate('Delete "%s"'), $nodeName) + ) . '</h2>' + ); + + $biLink = $view->qlink( + $nodeName, + 'businessprocess/node/impact', + array('name' => $node->getName()), + array('data-base-target' => '_next') + ); + $this->addHtml( + '<p>' . sprintf( + $view->escape( + $this->translate('Unsure? Show business impact of "%s"') + ), + $biLink + ) . '</p>' + ); + + if ($this->parentNode) { + $yesMsg = sprintf( + $this->translate('Delete from %s'), + $this->parentNode->getAlias() + ); + } else { + $yesMsg = sprintf( + $this->translate('Delete root node "%s"'), + $nodeName + ); + } + + $this->addElement('select', 'confirm', array( + 'label' => $this->translate('Are you sure?'), + 'required' => true, + 'description' => $this->translate( + 'Do you really want to delete this node?' + ), + 'multiOptions' => $this->optionalEnum(array( + 'no' => $this->translate('No'), + 'yes' => $yesMsg, + 'all' => sprintf($this->translate('Delete all occurrences of %s'), $nodeName), + )) + )); + } + + /** + * @param Node $node + * @return $this + */ + public function setNode(Node $node) + { + $this->node = $node; + return $this; + } + + /** + * @param BpNode|null $node + * @return $this + */ + public function setParentNode(BpNode $node = null) + { + $this->parentNode = $node; + return $this; + } + + public function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + + $confirm = $this->getValue('confirm'); + switch ($confirm) { + case 'yes': + $changes->deleteNode($this->node, $this->parentNode === null ? null : $this->parentNode->getName()); + break; + case 'all': + $changes->deleteNode($this->node); + break; + case 'no': + $this->setSuccessMessage($this->translate('Well, maybe next time')); + } + + switch ($confirm) { + case 'yes': + case 'all': + if ($this->successUrl === null) { + $this->successUrl = clone $this->getRequest()->getUrl(); + } + + $this->successUrl->getParams()->remove(array('action', 'deletenode')); + } + + // Trigger session desctruction to make sure it get's stored. + // TODO: figure out why this is necessary, might be an unclean shutdown on redirect + unset($changes); + + parent::onSuccess(); + } +} diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php new file mode 100644 index 0000000..bd1592b --- /dev/null +++ b/application/forms/EditNodeForm.php @@ -0,0 +1,315 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Module\Businessprocess\Web\Form\Element\IplStateOverrides; +use Icinga\Module\Businessprocess\Web\Form\Validator\HostServiceTermValidator; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Session\SessionNamespace; +use ipl\Html\Attributes; +use ipl\Html\FormattedString; +use ipl\Html\HtmlElement; +use ipl\Html\ValidHtml; +use ipl\I18n\Translation; +use ipl\Web\Compat\CompatForm; +use ipl\Web\FormElement\TermInput\ValidatedTerm; +use ipl\Web\Url; + +class EditNodeForm extends CompatForm +{ + use Translation; + + /** @var ?BpConfig */ + protected $bp; + + /** @var ?Node */ + protected $node; + + /** @var ?BpNode */ + protected $parent; + + /** @var SessionNamespace */ + protected $session; + + /** + * Set the affected configuration + * + * @param BpConfig $bp + * + * @return $this + */ + public function setProcess(BpConfig $bp): self + { + $this->bp = $bp; + + return $this; + } + + /** + * Set the affected node + * + * @param Node $node + * + * @return $this + */ + public function setNode(Node $node): self + { + $this->node = $node; + + $this->populate([ + 'node-search' => $node->getName(), + 'node-label' => $node->getAlias() + ]); + + return $this; + } + + /** + * Set the affected sub-process + * + * @param ?BpNode $node + * + * @return $this + */ + public function setParentNode(BpNode $node = null): self + { + $this->parent = $node; + + if ($this->node !== null) { + $stateOverrides = $this->parent->getStateOverrides($this->node->getName()); + if (! empty($stateOverrides)) { + $this->populate([ + 'overrideStates' => 'y', + 'stateOverrides' => $stateOverrides + ]); + } + } + + return $this; + } + + /** + * Set the user's session + * + * @param SessionNamespace $session + * + * @return $this + */ + public function setSession(SessionNamespace $session): self + { + $this->session = $session; + + return $this; + } + + /** + * Identify and return the node the user has chosen + * + * @return Node + */ + protected function identifyChosenNode(): Node + { + $userInput = $this->getPopulatedValue('node'); + $nodeName = $this->getPopulatedValue('node-search'); + $nodeLabel = $this->getPopulatedValue('node-label'); + + if ($nodeName && $userInput === $nodeLabel) { + // User accepted a suggestion and didn't change it manually + $node = $this->bp->getNode($nodeName); + } elseif ($userInput && (! $nodeLabel || $userInput !== $nodeLabel)) { + // User didn't choose a suggestion or changed it manually + $node = $this->bp->getNode(BpConfig::joinNodeName($userInput, 'Hoststatus')); + } else { + // If the search and user input are both empty, it can only be the initial value + $node = $this->node; + } + + return $node; + } + + protected function assemble() + { + $this->addHtml(new HtmlElement('h2', null, FormattedString::create( + $this->translate('Modify "%s"'), + $this->node->getAlias() ?? $this->node->getName() + ))); + + if ($this->node instanceof ServiceNode) { + $this->assembleServiceElements(); + } else { + $this->assembleHostElements(); + } + + $this->addElement('submit', 'btn_submit', [ + 'label' => $this->translate('Save Changes') + ]); + } + + protected function assembleServiceElements(): void + { + if ($this->bp->getBackend() instanceof MonitoringBackend) { + $suggestionsPath = 'businessprocess/suggestions/monitoring-service'; + } else { + $suggestionsPath = 'businessprocess/suggestions/icingadb-service'; + } + + $node = $this->identifyChosenNode(); + + $this->addHtml($this->createSearchInput( + $this->translate('Service'), + $node->getAlias() ?? $node->getName(), + $suggestionsPath + )); + + $this->addElement('checkbox', 'overrideStates', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Override Service State') + ]); + if ($this->getPopulatedValue('overrideStates') === 'y') { + $this->addElement(new IplStateOverrides('stateOverrides', [ + 'label' => $this->translate('State Overrides'), + 'options' => [ + 0 => $this->translate('OK'), + 1 => $this->translate('WARNING'), + 2 => $this->translate('CRITICAL'), + 3 => $this->translate('UNKNOWN'), + 99 => $this->translate('PENDING'), + ] + ])); + } + } + + protected function assembleHostElements(): void + { + if ($this->bp->getBackend() instanceof MonitoringBackend) { + $suggestionsPath = 'businessprocess/suggestions/monitoring-host'; + } else { + $suggestionsPath = 'businessprocess/suggestions/icingadb-host'; + } + + $node = $this->identifyChosenNode(); + + $this->addHtml($this->createSearchInput( + $this->translate('Host'), + $node->getAlias() ?? $node->getName(), + $suggestionsPath + )); + + $this->addElement('checkbox', 'overrideStates', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Override Host State') + ]); + if ($this->getPopulatedValue('overrideStates') === 'y') { + $this->addElement(new IplStateOverrides('stateOverrides', [ + 'label' => $this->translate('State Overrides'), + 'options' => [ + 0 => $this->translate('UP'), + 1 => $this->translate('DOWN'), + 99 => $this->translate('PENDING') + ] + ])); + } + } + + protected function createSearchInput(string $label, string $value, string $suggestionsPath): ValidHtml + { + $userInput = $this->createElement('text', 'node', [ + 'ignore' => true, + 'required' => true, + 'autocomplete' => 'off', + 'label' => $label, + 'value' => $value, + 'data-enrichment-type' => 'completion', + 'data-term-suggestions' => '#node-suggestions', + 'data-suggest-url' => Url::fromPath($suggestionsPath, [ + 'node' => isset($this->parent) ? $this->parent->getName() : null, + 'config' => $this->bp->getName(), + 'showCompact' => true, + '_disableLayout' => true + ]), + 'validators' => ['callback' => function ($_, $validator) { + $newName = $this->identifyChosenNode()->getName(); + if ($newName === $this->node->getName()) { + return true; + } + + $term = new ValidatedTerm($newName); + + (new HostServiceTermValidator()) + ->setParent($this->parent) + ->isValid($term); + + if (! $term->isValid()) { + $validator->addMessage($term->getMessage()); + return false; + } + + return true; + }] + ]); + + $fieldset = new HtmlElement('fieldset'); + + $searchInput = $this->createElement('hidden', 'node-search', ['ignore' => true]); + $this->registerElement($searchInput); + $fieldset->addHtml($searchInput); + + $labelInput = $this->createElement('hidden', 'node-label', ['ignore' => true]); + $this->registerElement($labelInput); + $fieldset->addHtml($labelInput); + + $this->registerElement($userInput); + $this->decorate($userInput); + + $fieldset->addHtml( + $userInput, + new HtmlElement('div', Attributes::create([ + 'id' => 'node-suggestions', + 'class' => 'search-suggestions' + ])) + ); + + return $fieldset; + } + + protected function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + + $children = $this->parent->getChildNames(); + $previousPos = array_search($this->node->getName(), $children, true); + $node = $this->identifyChosenNode(); + $nodeName = $node->getName(); + + $changes->deleteNode($this->node, $this->parent->getName()); + $changes->addChildrenToNode([$nodeName], $this->parent); + + $stateOverrides = $this->getValue('stateOverrides'); + if (! empty($stateOverrides)) { + $changes->modifyNode($this->parent, [ + 'stateOverrides' => array_merge($this->parent->getStateOverrides(), [ + $nodeName => $stateOverrides + ]) + ]); + } + + if ($this->bp->getMetadata()->isManuallyOrdered() && ($newPos = count($children) - 1) > $previousPos) { + $changes->moveNode( + $node, + $newPos, + $previousPos, + $this->parent->getName(), + $this->parent->getName() + ); + } + + unset($changes); + } +} diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php new file mode 100644 index 0000000..81d15c7 --- /dev/null +++ b/application/forms/MoveNodeForm.php @@ -0,0 +1,172 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Application\Icinga; +use Icinga\Application\Web; +use Icinga\Exception\Http\HttpException; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Exception\ModificationError; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; +use Icinga\Module\Businessprocess\Web\Form\CsrfToken; +use Icinga\Web\Session; +use Icinga\Web\Session\SessionNamespace; + +class MoveNodeForm extends BpConfigBaseForm +{ + /** @var BpConfig */ + protected $bp; + + /** @var Node */ + protected $node; + + /** @var BpNode */ + protected $parentNode; + + /** @var SessionNamespace */ + protected $session; + + public function __construct($options = null) + { + parent::__construct($options); + + // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying + // Zend paths + $this->addPrefixPaths(array( + array( + 'prefix' => 'Icinga\\Web\\Form\\Element\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'), + 'type' => static::ELEMENT + ), + array( + 'prefix' => 'Icinga\\Web\\Form\\Decorator\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'), + 'type' => static::DECORATOR + ) + )); + } + + public function setup() + { + $this->addElement( + 'text', + 'parent', + [ + 'allowEmpty' => true, + 'filters' => ['Null'], + 'validators' => [ + ['Callback', true, [ + 'callback' => function ($name) { + return empty($name) || $this->bp->hasBpNode($name); + }, + 'messages' => [ + 'callbackValue' => $this->translate('No process found with name %value%') + ] + ]] + ] + ] + ); + $this->addElement( + 'number', + 'from', + [ + 'required' => true, + 'min' => 0 + ] + ); + $this->addElement( + 'number', + 'to', + [ + 'required' => true, + 'min' => 0 + ] + ); + $this->addElement( + 'hidden', + 'csrfToken', + [ + 'required' => true + ] + ); + + $this->setSubmitLabel('movenode'); + } + + /** + * @param Node $node + * @return $this + */ + public function setNode(Node $node) + { + $this->node = $node; + return $this; + } + + /** + * @param BpNode|null $node + * @return $this + */ + public function setParentNode(BpNode $node = null) + { + $this->parentNode = $node; + return $this; + } + + public function onSuccess() + { + if (! CsrfToken::isValid($this->getValue('csrfToken'))) { + throw new HttpException(403, 'nope'); + } + + $changes = ProcessChanges::construct($this->bp, $this->session); + if (! $this->bp->getMetadata()->isManuallyOrdered()) { + $changes->applyManualOrder(); + } + + try { + $changes->moveNode( + $this->node, + $this->getValue('from'), + $this->getValue('to'), + $this->getValue('parent'), + $this->parentNode !== null ? $this->parentNode->getName() : null + ); + } catch (ModificationError $e) { + $this->notifyError($e->getMessage()); + /** @var Web $app */ + $app = Icinga::app(); + $app->getResponse() + // Web 2's JS forces a content update for non-200s. Our own JS + // can't prevent this, hence we're not making this a 400 :( + //->setHttpResponseCode(400) + ->setHeader('X-Icinga-Container', 'ignore') + ->sendResponse(); + exit; + } + + // Trigger session destruction to make sure it get's stored. + unset($changes); + + $this->notifySuccess($this->getSuccessMessage($this->translate('Node order updated'))); + + $response = $this->getRequest()->getResponse() + ->setHeader('X-Icinga-Container', 'ignore') + ->setHeader('X-Icinga-Extra-Updates', implode(';', [ + $this->getRequest()->getHeader('X-Icinga-Container'), + $this->getSuccessUrl()->getAbsoluteUrl() + ])); + + Session::getSession()->write(); + $response->sendResponse(); + exit; + } + + public function hasBeenSent() + { + return true; // This form has no id + } +} diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php new file mode 100644 index 0000000..126fe9b --- /dev/null +++ b/application/forms/ProcessForm.php @@ -0,0 +1,158 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; +use Icinga\Web\Notification; +use Icinga\Web\View; + +class ProcessForm extends BpConfigBaseForm +{ + /** @var BpNode */ + protected $node; + + public function setup() + { + if ($this->node !== null) { + /** @var View $view */ + $view = $this->getView(); + + $this->addHtml( + '<h2>' . $view->escape( + sprintf($this->translate('Modify "%s"'), $this->node->getAlias()) + ) . '</h2>' + ); + } + + $this->addElement('text', 'name', [ + 'label' => $this->translate('ID'), + 'value' => (string) $this->node, + 'required' => true, + 'readonly' => $this->node ? true : null, + 'description' => $this->translate('This is the unique identifier of this process') + ]); + + $this->addElement('text', 'alias', array( + 'label' => $this->translate('Display Name'), + 'description' => $this->translate( + 'Usually this name will be shown for this node. Equals ID' + . ' if not given' + ), + )); + + $this->addElement('select', 'operator', array( + 'label' => $this->translate('Operator'), + 'required' => true, + 'multiOptions' => Node::getOperators() + )); + + if ($this->node !== null) { + $display = $this->node->getDisplay() ?: 1; + } else { + $display = 1; + } + $this->addElement('select', 'display', array( + 'label' => $this->translate('Visualization'), + 'required' => true, + 'description' => $this->translate( + 'Where to show this process' + ), + 'multiOptions' => array( + "$display" => $this->translate('Toplevel Process'), + '0' => $this->translate('Subprocess only'), + ) + )); + + $this->addElement('text', 'url', array( + 'label' => $this->translate('Info URL'), + 'description' => $this->translate( + 'URL pointing to more information about this node' + ) + )); + + if ($node = $this->node) { + if ($node->hasAlias()) { + $this->getElement('alias')->setValue($node->getAlias()); + } + $this->getElement('operator')->setValue($node->getOperator()); + $this->getElement('display')->setValue($node->getDisplay()); + if ($node->hasInfoUrl()) { + $this->getElement('url')->setValue($node->getInfoUrl()); + } + } + } + + /** + * @param BpNode $node + * @return $this + */ + public function setNode(BpNode $node) + { + $this->node = $node; + return $this; + } + + public function onSuccess() + { + $changes = ProcessChanges::construct($this->bp, $this->session); + + $modifications = array(); + $alias = $this->getValue('alias'); + $operator = $this->getValue('operator'); + $display = $this->getValue('display'); + $url = $this->getValue('url'); + if (empty($url)) { + $url = null; + } + if (empty($alias)) { + $alias = null; + } + // TODO: rename + + if ($node = $this->node) { + if ($display !== $node->getDisplay()) { + $modifications['display'] = $display; + } + if ($operator !== $node->getOperator()) { + $modifications['operator'] = $operator; + } + if ($url !== $node->getInfoUrl()) { + $modifications['infoUrl'] = $url; + } + if ($alias !== $node->getAlias()) { + $modifications['alias'] = $alias; + } + } else { + $modifications = array( + 'display' => $display, + 'operator' => $operator, + 'infoUrl' => $url, + 'alias' => $alias, + ); + } + + if (! empty($modifications)) { + if ($this->node === null) { + $changes->createNode($this->getValue('name'), $modifications); + } else { + $changes->modifyNode($this->node, $modifications); + } + + Notification::success( + sprintf( + 'Process %s has been modified', + $this->bp->getName() + ) + ); + } + + // Trigger session destruction to make sure it get's stored. + // TODO: figure out why this is necessary, might be an unclean shutdown on redirect + unset($changes); + + parent::onSuccess(); + } +} diff --git a/application/forms/SimulationForm.php b/application/forms/SimulationForm.php new file mode 100644 index 0000000..04a0f56 --- /dev/null +++ b/application/forms/SimulationForm.php @@ -0,0 +1,138 @@ +<?php + +namespace Icinga\Module\Businessprocess\Forms; + +use Icinga\Module\Businessprocess\MonitoredNode; +use Icinga\Module\Businessprocess\Simulation; +use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm; +use Icinga\Web\View; + +class SimulationForm extends BpConfigBaseForm +{ + /** @var MonitoredNode */ + protected $node; + + /** @var ?MonitoredNode */ + protected $simulatedNode; + + /** @var Simulation */ + protected $simulation; + + public function setup() + { + $states = $this->enumStateNames(); + + // TODO: Fetch state from object + if ($this->simulatedNode) { + $simulatedState = $this->simulatedNode->getState(); + $states[$simulatedState] = sprintf( + '%s (%s)', + $this->node->getStateName($simulatedState), + $this->translate('Current simulation') + ); + $node = $this->simulatedNode; + $hasSimulation = true; + } else { + $hasSimulation = false; + $node = $this->node; + } + + /** @var View $view */ + $view = $this->getView(); + if ($hasSimulation) { + $title = $this->translate('Modify simulation for %s'); + } else { + $title = $this->translate('Add simulation for %s'); + } + $this->addHtml( + '<h2>' + . $view->escape(sprintf($title, $node->getAlias() ?? $node->getName())) + . '</h2>' + ); + + $this->addElement('select', 'state', array( + 'label' => $this->translate('State'), + 'multiOptions' => $states, + 'class' => 'autosubmit', + 'value' => $this->simulatedNode ? $node->getState() : null, + )); + + $sentState = $this->getSentValue('state'); + if (in_array($sentState, array('0', '99'))) { + return; + } + + if ($hasSimulation || ($sentState !== null && ctype_digit($sentState))) { + $this->addElement('checkbox', 'acknowledged', array( + 'label' => $this->translate('Acknowledged'), + 'value' => $node->isAcknowledged(), + )); + + $this->addElement('checkbox', 'in_downtime', array( + 'label' => $this->translate('In downtime'), + 'value' => $node->isInDowntime(), + )); + } + + $this->setSubmitLabel($this->translate('Apply')); + } + + public function setNode($node) + { + $this->node = $node; + return $this; + } + + public function setSimulation(Simulation $simulation) + { + $this->simulation = $simulation; + + $name = $this->node->getName(); + if ($simulation->hasNode($name)) { + $this->simulatedNode = clone($this->node); + $s = $simulation->getNode($name); + $this->simulatedNode->setState($s->state) + ->setAck($s->acknowledged) + ->setDowntime($s->in_downtime) + ->setMissing(false); + } + + return $this; + } + + public function onSuccess() + { + $nodeName = $this->node->getName(); + $state = $this->getValue('state'); + + if ($state !== null && ctype_digit($state)) { + $this->notifySuccess($this->translate('Simulation has been set')); + $this->simulation->set($nodeName, (object) array( + 'state' => $this->getValue('state'), + 'acknowledged' => $this->getValue('acknowledged'), + 'in_downtime' => $this->getValue('in_downtime'), + )); + } else { + if ($this->simulation->remove($nodeName)) { + $this->notifySuccess($this->translate('Simulation has been removed')); + } + } + + parent::onSuccess(); + } + + /** + * @return array + */ + protected function enumStateNames() + { + $states = array( + null => sprintf( + $this->translate('Use current state (%s)'), + $this->translate($this->node->getStateName()) + ) + ) + $this->node->enumStateNames(); + + return $states; + } +} diff --git a/application/views/helpers/FormSimpleNote.php b/application/views/helpers/FormSimpleNote.php new file mode 100644 index 0000000..d8315f4 --- /dev/null +++ b/application/views/helpers/FormSimpleNote.php @@ -0,0 +1,15 @@ +<?php + +// Avoid complaints about missing namespace and invalid class name +// @codingStandardsIgnoreStart +class Zend_View_Helper_FormSimpleNote extends Zend_View_Helper_FormElement +{ + // @codingStandardsIgnoreEnd + + public function formSimpleNote($name, $value = null) + { + $info = $this->_getInfo($name, $value); + extract($info); // name, value, attribs, options, listsep, disable + return $value; + } +} diff --git a/application/views/helpers/RenderStateBadges.php b/application/views/helpers/RenderStateBadges.php new file mode 100644 index 0000000..70633aa --- /dev/null +++ b/application/views/helpers/RenderStateBadges.php @@ -0,0 +1,33 @@ +<?php + +/** + * @deprecated + * @codingStandardsIgnoreStart + */ +class Zend_View_Helper_RenderStateBadges extends Zend_View_Helper_Abstract +{ + // @codingStandardsIgnoreEnd + public function renderStateBadges($summary) + { + $html = ''; + + foreach ($summary as $state => $cnt) { + if ($cnt === 0 + || $state === 'OK' + || $state === 'UP' + ) { + continue; + } + + $html .= '<span class="badge badge-' . strtolower($state) + . '" title="' . mt('monitoring', $state) . '">' + . $cnt . '</span>'; + } + + if ($html !== '') { + $html = '<div class="badges">' . $html . '</div>'; + } + + return $html; + } +} diff --git a/application/views/scripts/default.phtml b/application/views/scripts/default.phtml new file mode 100644 index 0000000..3e2cc59 --- /dev/null +++ b/application/views/scripts/default.phtml @@ -0,0 +1,2 @@ +<?= $this->controls->render() ?> +<?= $this->content->render() ?> diff --git a/application/views/scripts/host/show.phtml b/application/views/scripts/host/show.phtml new file mode 100644 index 0000000..413baf2 --- /dev/null +++ b/application/views/scripts/host/show.phtml @@ -0,0 +1,13 @@ +<?php +/** @var \Icinga\Web\View $this */ +/** @var \Icinga\Web\Widget\Tabs $tabs */ +/** @var string $host */ +?> +<div class="controls"> + <?= $tabs->showOnlyCloseButton() ?> +</div> +<div class="content restricted"> + <h1><?= $this->translate('Access Denied') ?></h1> + <p><?= sprintf($this->translate('You are lacking permission to access host "%s".'), $this->escape($host)) ?></p> + <a href="#" class="close-container-control action-link"><?= $this->icon('cancel') ?><?= $this->translate('Hide this message') ?></a> +</div> diff --git a/application/views/scripts/process/source.phtml b/application/views/scripts/process/source.phtml new file mode 100644 index 0000000..d5ba6bb --- /dev/null +++ b/application/views/scripts/process/source.phtml @@ -0,0 +1,25 @@ +<?= $this->controls->render() ?> + +<div class="content"> +<?php if ($this->showDiff): ?> +<div class="diff"> +<?= $this->diff->render() ?> +</div> +<?php else: ?> +<table class="sourcecode"> +<?php + +$cnt = 0; +$lines = preg_split('~\r?\n~', $this->source); +$len = ceil(log(count($lines), 10)); +$rowhtml = sprintf('<tr><th>%%0%dd: </th><td>%%s<br></td></tr>', $len); + +foreach ($lines as $line) { + $cnt++; + printf($rowhtml, $cnt, $this->escape($line)); +} + +?> +</table> +<?php endif ?> +</div> diff --git a/application/views/scripts/service/show.phtml b/application/views/scripts/service/show.phtml new file mode 100644 index 0000000..205b3f7 --- /dev/null +++ b/application/views/scripts/service/show.phtml @@ -0,0 +1,14 @@ +<?php +/** @var \Icinga\Web\View $this */ +/** @var \Icinga\Web\Widget\Tabs $tabs */ +/** @var string $host */ +/** @var string $service */ +?> +<div class="controls"> + <?= $tabs->showOnlyCloseButton() ?> +</div> +<div class="content restricted"> + <h1><?= $this->escape($this->translate('Access Denied')) ?></h1> + <p><?= $this->escape(sprintf($this->translate('You are lacking permission to access service "%s" on host "%s"'), $service, $host)) ?></p> + <a href="#" class="close-container-control action-link"><?= $this->icon('cancel') ?><?= $this->translate('Hide this message') ?></a> +</div> |