Version 2.5.0

-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEjeMGOyH1H9MofRhTOqKMoN1x5FcFAmUnn6sACgkQOqKMoN1x
 5FcqhQ/9Hcz52MHnnp6OBHOnUxEwToLS2yYqA0zFCqLYjwWzLWTg8B+YSd5DPHd+
 dK1QZs/8bNc8q4ebLzwOOx0V+QgpRhkO90ixtmJkGvQg5SBBQjWCJy5YIXm3qGh7
 Ce5hsWv2EGTgZMW9FsULcsierE7j4iWa5AqZ9b2Nz7vpSoUkTojOJRLEKGm9a6+g
 /EyePSAo0vgwkLX2lK3gR5Q6MFRKV/PY4sX+Bx+YQElz2yJK3A3joUEpYXjCQ7Gl
 c0SQ3XCCfxmxmVVSfOCEd2X45WAGWSoWmF7ZfB6dfkT9QmJZ5tAHqilxdU8tsEF+
 RnS+Jtl1DG/dN52xkDPUWfBzAqvQzupXZOAZ4MwUSAFEJazrAWMWddM1tdcXYKo5
 1ts1EXAE9OLHMbnnlK7KrxaZJfsyKktYt0WGLIE9KGaAnxqLwtaWz1QyoMMeHZBV
 RgaWVfixkjPZIajVRIhEnI9dWcXk7s1AeHgEoQpvFpIeBwdzBLKNAUjyTIBKd+Qk
 Wkbzq808UR4UyWASqlto5ce/ovYI7QvXB6nCLCBDlHvbh7YLQBqmshHEq2PMIuoS
 rD0xxYYRD32QJSGRI6svqOmLruogx5/BRP+Zq+3akhUaj6Gqke6v94QR8RsAFeHt
 z91B6wSjVtwzk5EaP6g+U4aUR57psHdpekG68KDYZuasnxTteGo=
 =ktL6
 -----END PGP SIGNATURE-----

Merge tag 'v2.5.0' into feature/dashboard

Version 2.5.0
This commit is contained in:
Navid Sassan 2023-11-07 10:35:25 +01:00
commit ba12733b1e
117 changed files with 7652 additions and 2399 deletions

View file

@ -3,7 +3,7 @@ name: L10n Update
on:
push:
branches:
- master
- main
jobs:
trigger-update:

View file

@ -3,11 +3,11 @@ name: PHP Tests
on:
push:
branches:
- master
- main
- release/*
pull_request:
branches:
- master
- main
jobs:
lint:
@ -17,12 +17,12 @@ jobs:
strategy:
fail-fast: false
matrix:
php: ['7.2', '7.3', '7.4', '8.0', '8.1']
php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
os: ['ubuntu-latest']
steps:
- name: Checkout code base
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
@ -31,12 +31,65 @@ jobs:
tools: phpcs
- name: Setup dependencies
run: composer require -n --no-progress overtrue/phplint
run: |
composer require -n --no-progress overtrue/phplint
git clone --depth 1 https://github.com/Icinga/icingaweb2.git vendor/icingaweb2
git clone --depth 1 https://github.com/Icinga/icingadb-web.git vendor/icingadb-web
git clone --depth 1 https://github.com/Icinga/icingaweb2-module-director.git vendor/icingaweb2-module-director
git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-library.git vendor/icinga-php-library
git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git vendor/icinga-php-thirdparty
- name: PHP Lint
if: success() || matrix.allow_failure
if: ${{ ! cancelled() }}
run: ./vendor/bin/phplint -n --exclude={^vendor/.*} -- .
- name: PHP CodeSniffer
if: success() || matrix.allow_failure
if: ${{ ! cancelled() }}
run: phpcs
- name: PHPStan
if: ${{ ! cancelled() }}
uses: php-actions/phpstan@v3
test:
name: Unit tests with php ${{ matrix.php }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
env:
phpunit-version: 8.5
strategy:
fail-fast: false
matrix:
php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
os: ['ubuntu-latest']
steps:
- name: Checkout code base
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: phpunit:${{ matrix.phpunit-version || env.phpunit-version }}
- name: Setup Icinga Web
run: |
git clone --depth 1 https://github.com/Icinga/icingaweb2.git _icingaweb2
ln -s `pwd` _icingaweb2/modules/businessprocess
- name: Setup Libraries
run: |
mkdir _libraries
git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-library.git _libraries/ipl
git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git _libraries/vendor
- name: Setup dependencies
run: composer require -d _icingaweb2 -n --no-progress mockery/mockery
- name: PHPUnit
env:
ICINGAWEB_LIBDIR: _libraries
ICINGAWEB_CONFIGDIR: test/config
run: phpunit --verbose --bootstrap _icingaweb2/test/php/bootstrap.php

View file

@ -1,86 +0,0 @@
stages:
- Coding Standards
- Unit-Tests
- Build Packages
variables:
BASE_VERSION: "2.0.0"
VERSION_SUFFIX: "-b${CI_BUILD_ID}-${CI_BUILD_REF_SLUG}"
PSR2 CS Test:
stage: Coding Standards
tags:
- xenial
script:
- phpcs --report-width=auto --report-full --report-gitblame --report-summary -p --standard=PSR2 --extensions=php --encoding=utf-8 -w -s library/Businessprocess/ application/ configuration.php run.php test
Ubuntu Xenial:
stage: Unit-Tests
tags:
- xenial
- businessprocess
script:
- phpunit --testdox --coverage-html=coverage || phpunit --verbose
artifacts:
expire_in: 1 week
name: code-coverage
paths:
- coverage/*
Debian Jessie:
stage: Unit-Tests
tags:
- jessie
- businessprocess
script:
- phpunit --testdox || phpunit --verbose
CentOS 6:
stage: Unit-Tests
tags:
- centos6
- businessprocess
script:
- phpunit --testdox || phpunit --verbose
CentOS 7:
stage: Unit-Tests
tags:
- centos7
- businessprocess
script:
- phpunit --testdox || phpunit --verbose
Xenial Packages:
stage: Build Packages
tags:
- xenial
- businessprocess
script:
- cp -a packaging/debian debian
- dch --no-conf -U -M --empty -v "${BASE_VERSION}${VERSION_SUFFIX}-${CI_BUILD_REF:0:7}" "Automated build triggered by ${GITLAB_USER_ID} <${GITLAB_USER_EMAIL}>"
- cp LICENSE debian/copyright
- dpkg-buildpackage -us -uc
- mkdir build
- mv ../icingaweb2-module-businessprocess*.deb build/
artifacts:
expire_in: 1 week
paths:
- build/*
Jessie Packages:
stage: Build Packages
tags:
- jessie
- businessprocess
script:
- cp -a packaging/debian debian
- dch --no-conf -U -M --empty -v "${BASE_VERSION}${VERSION_SUFFIX}-${CI_BUILD_REF:0:7}" "Automated build triggered by ${GITLAB_USER_ID} <${GITLAB_USER_EMAIL}>"
- cp LICENSE debian/copyright
- dpkg-buildpackage -us -uc
- mkdir build
- mv ../icingaweb2-module-businessprocess*.deb build/
artifacts:
expire_in: 1 week
paths:
- build/*

View file

@ -1,31 +0,0 @@
language: php
php:
- '5.4'
- '5.5'
- '5.6'
- '7.0'
- '7.1'
- '7.2'
- nightly
matrix:
fast_finish: true
allow_failures:
- php: '5.4'
- php: '5.5'
- php: nightly
cache:
directories:
- vendor
env:
- ICINGAWEB_VERSION=2.6.2
- IPL_VERSION=0.1.1
before_script:
- ./test/setup_vendor.sh
script:
- php vendor/phpcs.phar
- php vendor/phpunit.phar --testdox || php vendor/phpunit.phar --verbose

View file

@ -1,8 +1,8 @@
# Icinga Business Process Modeling
[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/)
![Build Status](https://github.com/icinga/icingaweb2-module-businessprocess/workflows/PHP%20Tests/badge.svg?branch=master)
[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-businessprocess.svg)](https://github.com/Icinga/icingaweb2-module-businessprocess)
[![Build Status](https://github.com/Icinga/icingaweb2-module-businessprocess/actions/workflows/php.yml/badge.svg)](https://github.com/Icinga/icingaweb2-module-businessprocess/actions/workflows/php.yml)
[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-businessprocess.svg)](https://github.com/Icinga/icingaweb2-module-businessprocess/releases/latest)
![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png)

View file

@ -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";
}
}
}

View file

@ -110,8 +110,8 @@ class ProcessCommand extends Command
exit(1);
}
$name = $this->params->get('config');
try {
$name = $this->params->get('config');
if ($name === null) {
$name = $this->getFirstProcessName();
}
@ -132,8 +132,8 @@ class ProcessCommand extends Command
}
}
/** @var BpNode $node */
try {
/** @var BpNode $node */
$node = $bp->getNode($nodeName);
if (Module::exists('icingadb')
&& (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())

View file

@ -7,6 +7,7 @@ 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;
@ -54,7 +55,8 @@ class HostController extends Controller
->from('hoststatus', array('host_name'))
->where('host_name', $hostName);
if ($this->applyRestriction('monitoring/filter/objects', $query)->fetchRow() !== false) {
$this->applyRestriction('monitoring/filter/objects', $query);
if ($query->fetchRow() !== false) {
$this->redirectNow(Url::fromPath('monitoring/host/show')->setParams($this->params));
}
}

View file

@ -2,6 +2,7 @@
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;
@ -11,6 +12,8 @@ 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
{
@ -24,9 +27,16 @@ class NodeController extends Controller
$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) {
$config = $this->storage()->loadProcess($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)) {
@ -108,5 +118,31 @@ class NodeController extends Controller
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);
}
}
}

View file

@ -6,6 +6,8 @@ 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;
@ -26,8 +28,16 @@ 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
{
@ -114,15 +124,7 @@ class ProcessController extends Controller
$this->tabs()->extend(new OutputFormat());
$missing = $bp->getMissingChildren();
if (! empty($missing)) {
if (($count = count($missing)) > 10) {
$missing = array_slice($missing, 0, 10);
$missing[] = '...';
}
$bp->addError('There are %d missing nodes: %s', $count, implode(', ', $missing));
}
$this->content()->add($this->showHints($bp));
$this->content()->add($this->showHints($bp, $renderer));
$this->content()->add($this->showWarnings($bp));
$this->content()->add($this->showErrors($bp));
$this->content()->add($renderer);
@ -130,6 +132,50 @@ class ProcessController extends Controller
$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();
@ -140,10 +186,9 @@ class ProcessController extends Controller
'a',
[
'href' => $this->url()->without('showFullscreen')->without('view'),
'title' => $this->translate('Leave full screen and switch back to normal mode'),
'style' => 'float: right'
'title' => $this->translate('Leave full screen and switch back to normal mode')
],
Html::tag('i', ['class' => 'icon icon-resize-small'])
new Icon('down-left-and-up-right-to-center')
));
}
@ -155,9 +200,11 @@ class ProcessController extends Controller
$controls->add(Breadcrumb::create(clone $renderer));
if (! $this->showFullscreen && ! $this->view->compact) {
$controls->add(
new RenderedProcessActionBar($bp, $renderer, $this->Auth(), $this->url())
new RenderedProcessActionBar($bp, $renderer, $this->url())
);
}
$controls->addHtml($this->createBpSortControl($renderer, $bp));
}
protected function getNode(BpConfig $bp)
@ -225,21 +272,43 @@ class ProcessController extends Controller
$canEdit = $bp->getMetadata()->canModify();
if ($action === 'add' && $canEdit) {
$form = $this->loadForm('AddNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
->setStorage($this->storage())
$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 = $this->loadForm('EditNode')
->setSuccessUrl(Url::fromRequest()->without('action'))
$form = (new EditNodeForm())
->setProcess($bp)
->setNode($bp->getNode($this->params->get('editmonitorednode')))
->setParentNode($node)
->setSession($this->session())
->handleRequest();
->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'))
@ -262,7 +331,21 @@ class ProcessController extends Controller
->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())
@ -285,8 +368,11 @@ class ProcessController extends Controller
return;
}
if ($this->params->get('action')) {
$this->setAutorefreshInterval(45);
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);
}
@ -320,12 +406,14 @@ class ProcessController extends Controller
}
}
protected function showHints(BpConfig $bp)
protected function showHints(BpConfig $bp, Renderer $renderer)
{
$ul = Html::tag('ul', ['class' => 'error']);
$this->prepareMissingNodeLinks($ul);
foreach ($bp->getErrors() as $error) {
$ul->add(Html::tag('li')->setContent($error));
$ul->addHtml(Html::tag('li', $error));
}
if ($bp->hasChanges()) {
$li = Html::tag('li')->setSeparator(' ');
$li->add(sprintf(
@ -359,6 +447,20 @@ class ProcessController extends Controller
$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 {
@ -366,6 +468,66 @@ class ProcessController extends Controller
}
}
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
*/
@ -447,7 +609,7 @@ class ProcessController extends Controller
->setParams($this->getRequest()->getUrl()->getParams());
$this->content()->add(
$this->loadForm('bpConfig')
->setProcessConfig($bp)
->setProcess($bp)
->setStorage($this->storage())
->setSuccessUrl($url)
->handleRequest()
@ -464,10 +626,12 @@ class ProcessController extends Controller
'a',
[
'href' => Url::fromPath('businessprocess/process/source', $params),
'class' => 'icon-doc-text',
'title' => $this->translate('Show source code')
],
$this->translate('Source')
[
new Icon('file-lines'),
$this->translate('Source'),
]
));
} else {
$params = array(
@ -479,10 +643,12 @@ class ProcessController extends Controller
'a',
[
'href' => Url::fromPath('businessprocess/process/source', $params),
'class' => 'icon-flapping',
'title' => $this->translate('Highlight changes')
],
$this->translate('Diff')
[
new Icon('shuffle'),
$this->translate('Diff')
]
));
}
@ -490,11 +656,13 @@ class ProcessController extends Controller
'a',
[
'href' => Url::fromPath('businessprocess/process/download', ['config' => $config->getName()]),
'class' => 'icon-download',
'target' => '_blank',
'title' => $this->translate('Download process configuration')
],
$this->translate('Download')
[
new Icon('download'),
$this->translate('Download')
]
));
return $actionBar;
@ -584,7 +752,7 @@ class ProcessController extends Controller
if (isset($node['since'])) {
$data[] = DateFormatter::formatDateTime($node['since']);
}
if (isset($node['in_downtime'])) {
$data[] = $node['in_downtime'];
}

View file

@ -7,6 +7,7 @@ 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;
@ -61,7 +62,8 @@ class ServiceController extends Controller
->where('host_name', $hostName)
->where('service_description', $serviceName);
if ($this->applyRestriction('monitoring/filter/objects', $query)->fetchRow() !== false) {
$this->applyRestriction('monitoring/filter/objects', $query);
if ($query->fetchRow() !== false) {
$this->redirectNow(Url::fromPath('monitoring/service/show')->setParams($this->params));
}
}

View file

@ -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()));
}
}

View file

@ -2,578 +2,411 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Exception;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\ImportedNode;
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\QuickForm;
use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidator;
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\Sql\Connection as IcingaDbConnection;
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 QuickForm
class AddNodeForm extends CompatForm
{
use EnumList;
/** @var MonitoringBackend|IcingaDbConnection*/
protected $backend;
use Sort;
use Translation;
/** @var Storage */
protected $storage;
/** @var BpConfig */
/** @var ?BpConfig */
protected $bp;
/** @var BpNode */
/** @var ?BpNode */
protected $parent;
protected $objectList = array();
protected $processList = array();
/** @var SessionNamespace */
protected $session;
public function setup()
{
$view = $this->getView();
if ($this->hasParentNode()) {
$this->addHtml(
'<h2>' . $view->escape(
sprintf($this->translate('Add a node to %s'), $this->parent->getAlias())
) . '</h2>'
);
} else {
$this->addHtml(
'<h2>' . $this->translate('Add a new root node') . '</h2>'
);
}
$type = $this->selectNodeType();
switch ($type) {
case 'host':
$this->selectHost();
break;
case 'service':
$this->selectService();
break;
case 'process':
$this->selectProcess();
break;
case 'new-process':
$this->addNewProcess();
break;
case 'hosts_from_filter':
$this->selectHostsFromFilter();
break;
case 'services_from_filter':
$this->selectServicesFromFilter();
break;
case null:
$this->setSubmitLabel($this->translate('Next'));
return;
}
}
protected function addNewProcess()
{
$this->addElement('text', 'name', array(
'label' => $this->translate('ID'),
'required' => true,
'description' => $this->translate(
'This is the unique identifier of this process'
),
'validators' => [
['Callback', true, [
'callback' => function ($value) {
if ($this->hasParentNode()) {
return ! $this->parent->hasChild($value);
}
return ! $this->bp->hasRootNode($value);
},
'messages' => [
'callbackValue' => $this->translate('%value% is already defined in 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' => array(
'&' => $this->translate('AND'),
'|' => $this->translate('OR'),
'!' => $this->translate('NOT'),
'%' => $this->translate('DEGRADED'),
'1' => $this->translate('MIN 1'),
'2' => $this->translate('MIN 2'),
'3' => $this->translate('MIN 3'),
'4' => $this->translate('MIN 4'),
'5' => $this->translate('MIN 5'),
'6' => $this->translate('MIN 6'),
'7' => $this->translate('MIN 7'),
'8' => $this->translate('MIN 8'),
'9' => $this->translate('MIN 9'),
)
));
$display = 1;
if ($this->bp->getMetadata()->isManuallyOrdered() && !$this->bp->isEmpty()) {
$rootNodes = $this->bp->getRootNodes();
$display = end($rootNodes)->getDisplay() + 1;
}
$this->addElement('select', 'display', array(
'label' => $this->translate('Visualization'),
'required' => true,
'description' => $this->translate(
'Where to show this process'
),
'value' => $this->hasParentNode() ? '0' : "$display",
'multiOptions' => array(
"$display" => $this->translate('Toplevel Process'),
'0' => $this->translate('Subprocess only'),
)
));
$this->addElement('text', 'infoUrl', array(
'label' => $this->translate('Info URL'),
'description' => $this->translate(
'URL pointing to more information about this node'
)
));
}
/**
* @return string|null
*/
protected function selectNodeType()
{
$types = array();
if ($this->hasParentNode()) {
$types['host'] = $this->translate('Host');
$types['service'] = $this->translate('Service');
$types['hosts_from_filter'] = $this->translate('Hosts from filter');
$types['services_from_filter'] = $this->translate('Services from filter');
} elseif (! $this->hasProcesses()) {
$this->addElement('hidden', 'node_type', array(
'ignore' => true,
'decorators' => array('ViewHelper'),
'value' => 'new-process'
));
return 'new-process';
}
if ($this->hasProcesses() || ($this->hasParentNode() && $this->hasMoreConfigs())) {
$types['process'] = $this->translate('Existing Process');
}
$types['new-process'] = $this->translate('New Process Node');
$this->addElement('select', 'node_type', array(
'label' => $this->translate('Node type'),
'required' => true,
'description' => $this->translate(
'The node type you want to add'
),
'ignore' => true,
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($types)
));
return $this->getSentValue('node_type');
}
protected function selectHost()
{
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Hosts'),
'required' => true,
'size' => 8,
'style' => 'width: 25em',
'multiOptions' => $this->enumHostList(),
'description' => $this->translate(
'Hosts that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
$this->addHostOverrideCheckbox();
if ($this->getSentValue('host_override') === '1') {
$this->addHostOverrideElement();
}
}
protected function selectService()
{
$this->addHostElement();
if ($host = $this->getSentValue('host')) {
$this->addServicesElement($host);
$this->addServiceOverrideCheckbox();
if ($this->getSentValue('service_override') === '1') {
$this->addServiceOverrideElement();
}
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function addHostElement()
{
$this->addElement('select', 'host', array(
'label' => $this->translate('Host'),
'required' => true,
'ignore' => true,
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($this->enumHostForServiceList()),
));
}
protected function addServicesElement($host)
{
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Services'),
'required' => true,
'size' => 8,
'style' => 'width: 25em',
'multiOptions' => $this->enumServiceList($host),
'description' => $this->translate(
'Services that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
}
protected function addFilteredHostsElement($filter)
{
$this->addElement('submit', 'refresh', [
'label' => $this->translate('Refresh'),
'class' => 'refresh-filter'
]);
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Hosts'),
'required' => true,
'size' => 8,
'style' => 'width: 25em',
'multiOptions' => $this->enumHostListByFilter($filter),
'description' => $this->translate(
'Hosts that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
}
protected function addFilteredServicesElement($filter)
{
$this->addElement('submit', 'refresh', [
'label' => $this->translate('Refresh'),
'class' => 'refresh-filter'
]);
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Services'),
'required' => true,
'size' => 8,
'style' => 'width: 25em',
'multiOptions' => $this->enumServiceListByFilter($filter),
'description' => $this->translate(
'Services that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
}
protected function addFilterElement()
{
$this->addElement('text', 'filter', array(
'label' => $this->translate('Filter'),
'required' => true,
'ignore' => true
));
}
protected function addFileElement()
{
$this->addElement('select', 'file', [
'label' => $this->translate('File'),
'required' => true,
'ignore' => true,
'value' => $this->bp->getName(),
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($this->enumConfigs()),
'description' => $this->translate(
'Choose a different configuration file to import its processes'
)
]);
}
protected function addHostOverrideCheckbox()
{
$this->addElement('checkbox', 'host_override', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Host State'),
'description' => $this->translate('Enable host state overrides')
]);
}
protected function addHostOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'label' => $this->translate('State Overrides'),
'states' => $this->enumHostStateList()
]);
}
protected function addServiceOverrideCheckbox()
{
$this->addElement('checkbox', 'service_override', [
'ignore' => true,
'class' => 'autosubmit',
'label' => $this->translate('Override Service State'),
'description' => $this->translate('Enable service state overrides')
]);
}
protected function addServiceOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'label' => $this->translate('State Overrides'),
'states' => $this->enumServiceStateList()
]);
}
protected function selectHostsFromFilter()
{
$this->addFilterElement();
if ($filter = $this->getSentValue('filter')) {
$this->addFilteredHostsElement($filter);
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function selectServicesFromFilter()
{
$this->addFilterElement();
if ($filter = $this->getSentValue('filter')) {
$this->addFilteredServicesElement($filter);
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function selectProcess()
{
if ($this->hasParentNode()) {
$this->addFileElement();
}
if (($file = $this->getSentValue('file')) || !$this->hasParentNode()) {
$this->addElement('multiselect', 'children', [
'label' => $this->translate('Process nodes'),
'required' => true,
'size' => 8,
'style' => 'width: 25em',
'multiOptions' => $this->enumProcesses($file),
'description' => $this->translate(
'Other processes that should be part of this business process node'
),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
]);
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* Set the storage to use
*
* @param Storage $storage
*
* @return $this
*/
public function setStorage(Storage $storage)
public function setStorage(Storage $storage): self
{
$this->storage = $storage;
return $this;
}
/**
* @param BpConfig $process
* Set the affected configuration
*
* @param BpConfig $bp
*
* @return $this
*/
public function setProcess(BpConfig $process)
public function setProcess(BpConfig $bp): self
{
$this->bp = $process;
$this->setBackend($process->getBackend());
$this->bp = $bp;
return $this;
}
/**
* @param BpNode|null $node
* Set the affected sub-process
*
* @param ?BpNode $node
*
* @return $this
*/
public function setParentNode(BpNode $node = null)
public function setParentNode(BpNode $node = null): self
{
$this->parent = $node;
return $this;
}
/**
* @return bool
*/
public function hasParentNode()
{
return $this->parent !== null;
}
/**
* Set the user's session
*
* @param SessionNamespace $session
*
* @return $this
*/
public function setSession(SessionNamespace $session)
public function setSession(SessionNamespace $session): self
{
$this->session = $session;
return $this;
}
protected function hasProcesses()
protected function assemble()
{
return count($this->enumProcesses()) > 0;
}
/**
* @param string $file
* @return array
*/
protected function enumProcesses($file = null)
{
$list = array();
$parents = array();
$differentFile = $file !== null && $file !== $this->bp->getName();
if (! $differentFile && $this->hasParentNode()) {
$this->collectAllParents($this->parent, $parents);
$parents[$this->parent->getName()] = $this->parent;
}
$bp = $this->bp;
if ($differentFile) {
$bp = $this->storage->loadProcess($file);
}
foreach ($bp->getNodes() as $node) {
if (! $node instanceof ImportedNode && $node instanceof BpNode && ! isset($parents[$node->getName()])) {
$name = $node->getName();
if ($differentFile) {
$name = '@' . $file . ':' . $name;
}
$list[$name] = $node->getName(); // display name?
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 = [];
}
}
if (! $this->bp->getMetadata()->isManuallyOrdered()) {
natcasesort($list);
$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';
}
return $list;
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 hasMoreConfigs()
protected function assembleNewProcessElements(): void
{
$configs = $this->enumConfigs();
return !empty($configs);
$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 enumConfigs()
protected function assembleExistingProcessElements(): void
{
return $this->storage->listProcesses();
$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)
);
}
/**
* Collect the given node's parents recursively into the given array by their names
*
* @param BpNode $node
* @param BpNode[] $parents
*/
protected function collectAllParents(BpNode $node, array &$parents)
protected function assembleHostElements(): void
{
foreach ($node->getParents() as $parent) {
$parents[$parent->getName()] = $parent;
$this->collectAllParents($parent, $parents);
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')
]
]));
}
}
public function onSuccess()
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);
switch ($this->getValue('node_type')) {
case 'host':
case 'service':
$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 ($this->getValue('children') as $service) {
$childOverrides[$service] = $stateOverrides;
foreach ($children as $nodeName) {
$childOverrides[$nodeName] = $stateOverrides;
}
$changes->modifyNode($this->parent, [
'stateOverrides' => array_merge($this->parent->getStateOverrides(), $childOverrides)
]);
}
}
// Fallthrough
case 'process':
case 'hosts_from_filter':
case 'services_from_filter':
if ($this->hasParentNode()) {
$changes->addChildrenToNode($this->getValue('children'), $this->parent);
} else {
foreach ($this->getValue('children') as $nodeName) {
$changes->copyNode($nodeName);
}
if ($this->parent !== null) {
$changes->addChildrenToNode($children, $this->parent);
} else {
foreach ($children as $nodeName) {
$changes->copyNode($nodeName);
}
break;
case 'new-process':
$properties = $this->getValues();
unset($properties['name']);
if ($this->hasParentNode()) {
$properties['parentName'] = $this->parent->getName();
}
$changes->createNode($this->getValue('name'), $properties);
break;
}
}
// 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();
}
}

View file

@ -23,12 +23,19 @@ class BpConfigForm extends BpConfigBaseForm
'max' => 40
)
),
array(
[
'validator' => 'Regex',
'options' => array(
'pattern' => '/^[a-zA-Z0-9](?:[a-zA-Z0-9 ._-]*)?[a-zA-Z0-9_]$/'
)
)
'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'
@ -109,12 +116,12 @@ class BpConfigForm extends BpConfigBaseForm
),
));
if ($this->config === null) {
if ($this->bp === null) {
$this->setSubmitLabel(
$this->translate('Add')
);
} else {
$config = $this->config;
$config = $this->bp;
$meta = $config->getMetadata();
foreach ($meta->getProperties() as $k => $v) {
@ -149,13 +156,13 @@ class BpConfigForm extends BpConfigBaseForm
$name = $this->getValue('name');
if ($this->shouldBeDeleted()) {
if ($this->config->isReferenced()) {
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->config->clearAppliedChanges();
$this->bp->clearAppliedChanges();
$this->storage->deleteProcess($name);
$this->setSuccessUrl('businessprocess');
$this->redirectOnSuccess(sprintf('Process %s has been deleted', $name));
@ -167,7 +174,7 @@ class BpConfigForm extends BpConfigBaseForm
{
$name = $this->getValue('name');
if ($this->config === null) {
if ($this->bp === null) {
if ($this->storage->hasProcess($name)) {
$this->addError(sprintf(
$this->translate('A process named "%s" already exists'),
@ -192,7 +199,7 @@ class BpConfigForm extends BpConfigBaseForm
);
$this->setSuccessMessage(sprintf('Process %s has been created', $name));
} else {
$config = $this->config;
$config = $this->bp;
$this->setSuccessMessage(sprintf('Process %s has been stored', $name));
}
$meta = $config->getMetadata();

View file

@ -10,8 +10,6 @@ use Icinga\Web\Notification;
class BpUploadForm extends BpConfigBaseForm
{
protected $backend;
protected $node;
protected $objectList = array();
@ -49,12 +47,19 @@ class BpUploadForm extends BpConfigBaseForm
'max' => 40
)
),
array(
[
'validator' => 'Regex',
'options' => array(
'pattern' => '/^[a-zA-Z0-9](?:[a-zA-Z0-9 ._-]*)?[a-zA-Z0-9_]$/'
)
)
'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.'
)
]
]
]
),
));
@ -150,7 +155,7 @@ class BpUploadForm extends BpConfigBaseForm
protected function processUploadedSource()
{
/** @var \Zend_Form_Element_File $el */
/** @var ?\Zend_Form_Element_File $el */
$el = $this->getElement('uploaded_file');
if ($el && $this->hasBeenSent()) {

View file

@ -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();
}
}

View file

@ -3,43 +3,34 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Web\View;
class DeleteNodeForm extends QuickForm
class DeleteNodeForm extends BpConfigBaseForm
{
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
protected $bp;
/** @var Node */
protected $node;
/** @var BpNode */
/** @var ?BpNode */
protected $parentNode;
/** @var SessionNamespace */
protected $session;
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"'), $node->getAlias())
sprintf($this->translate('Delete "%s"'), $nodeName)
) . '</h2>'
);
$biLink = $view->qlink(
$node->getAlias(),
$nodeName,
'businessprocess/node/impact',
array('name' => $node->getName()),
array('data-base-target' => '_next')
@ -61,7 +52,7 @@ class DeleteNodeForm extends QuickForm
} else {
$yesMsg = sprintf(
$this->translate('Delete root node "%s"'),
$this->node->getAlias()
$nodeName
);
}
@ -74,32 +65,11 @@ class DeleteNodeForm extends QuickForm
'multiOptions' => $this->optionalEnum(array(
'no' => $this->translate('No'),
'yes' => $yesMsg,
'all' => sprintf($this->translate('Delete all occurrences of %s'), $node->getAlias()),
'all' => sprintf($this->translate('Delete all occurrences of %s'), $nodeName),
))
));
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param Node $node
* @return $this
@ -120,16 +90,6 @@ class DeleteNodeForm extends QuickForm
return $this;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
public function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);

View file

@ -2,459 +2,314 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\EnumList;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Businessprocess\Web\Form\Validator\NoDuplicateChildrenValidator;
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\Sql\Connection as IcingaDbConnection;
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 QuickForm
class EditNodeForm extends CompatForm
{
use EnumList;
use Translation;
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
/** @var ?BpConfig */
protected $bp;
/** @var Node */
/** @var ?Node */
protected $node;
/** @var BpNode */
/** @var ?BpNode */
protected $parent;
protected $objectList = array();
protected $processList = array();
protected $service;
protected $host;
/** @var SessionNamespace */
protected $session;
public function setup()
{
$this->host = substr($this->getNode()->getName(), 0, strpos($this->getNode()->getName(), ';'));
if ($this->isService()) {
$this->service = substr($this->getNode()->getName(), strpos($this->getNode()->getName(), ';') + 1);
}
$view = $this->getView();
$this->addHtml(
'<h2>' . $view->escape(
sprintf($this->translate('Modify "%s"'), $this->getNode()->getAlias())
) . '</h2>'
);
$monitoredNodeType = null;
if ($this->isService()) {
$monitoredNodeType = 'service';
} else {
$monitoredNodeType = 'host';
}
$type = $this->selectNodeType($monitoredNodeType);
switch ($type) {
case 'host':
$this->selectHost();
break;
case 'service':
$this->selectService();
break;
case 'process':
$this->selectProcess();
break;
case 'new-process':
$this->addNewProcess();
break;
case null:
$this->setSubmitLabel($this->translate('Next'));
return;
}
}
protected function isService()
{
if (strpos($this->getNode()->getName(), ';Hoststatus')) {
return false;
}
return true;
}
protected function addNewProcess()
{
$this->addElement('text', 'name', array(
'label' => $this->translate('ID'),
'required' => true,
'disabled' => true,
'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' => array(
'&' => $this->translate('AND'),
'|' => $this->translate('OR'),
'!' => $this->translate('NOT'),
'%' => $this->translate('DEGRADED'),
'1' => $this->translate('MIN 1'),
'2' => $this->translate('MIN 2'),
'3' => $this->translate('MIN 3'),
'4' => $this->translate('MIN 4'),
'5' => $this->translate('MIN 5'),
'6' => $this->translate('MIN 6'),
'7' => $this->translate('MIN 7'),
'8' => $this->translate('MIN 8'),
'9' => $this->translate('MIN 9'),
)
));
$display = $this->getNode()->getDisplay() ?: 1;
$this->addElement('select', 'display', array(
'label' => $this->translate('Visualization'),
'required' => true,
'description' => $this->translate(
'Where to show this process'
),
'value' => $display,
'multiOptions' => array(
"$display" => $this->translate('Toplevel Process'),
'0' => $this->translate('Subprocess only'),
)
));
$this->addElement('text', 'infoUrl', array(
'label' => $this->translate('Info URL'),
'description' => $this->translate(
'URL pointing to more information about this node'
)
));
}
/**
* @return string|null
*/
protected function selectNodeType($monitoredNodeType = null)
{
if ($this->hasParentNode()) {
$this->addElement('hidden', 'node_type', [
'disabled' => true,
'decorators' => ['ViewHelper'],
'value' => $monitoredNodeType
]);
return $monitoredNodeType;
} elseif (! $this->hasProcesses()) {
$this->addElement('hidden', 'node_type', array(
'ignore' => true,
'decorators' => array('ViewHelper'),
'value' => 'new-process'
));
return 'new-process';
}
}
protected function selectHost()
{
$this->addElement('select', 'children', array(
'required' => true,
'value' => $this->getNode()->getName(),
'multiOptions' => $this->enumHostList(),
'label' => $this->translate('Host'),
'description' => $this->translate('The host for this business process node'),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
));
$this->addHostOverrideCheckbox();
$hostOverrideSent = $this->getSentValue('host_override');
if ($hostOverrideSent === '1'
|| ($hostOverrideSent === null && $this->getElement('host_override')->isChecked())
) {
$this->addHostOverrideElement();
}
}
protected function selectService()
{
$this->addHostElement();
if ($this->getSentValue('hosts') === null) {
$this->addServicesElement($this->host);
$this->addServiceOverrideCheckbox();
if ($this->getElement('service_override')->isChecked() || $this->getSentValue('service_override') === '1') {
$this->addServiceOverrideElement();
}
} elseif ($host = $this->getSentValue('hosts')) {
$this->addServicesElement($host);
$this->addServiceOverrideCheckbox();
if ($this->getSentValue('service_override') === '1') {
$this->addServiceOverrideElement();
}
} else {
$this->setSubmitLabel($this->translate('Next'));
}
}
protected function addHostElement()
{
$this->addElement('select', 'hosts', array(
'label' => $this->translate('Host'),
'required' => true,
'ignore' => true,
'class' => 'autosubmit',
'multiOptions' => $this->optionalEnum($this->enumHostForServiceList()),
));
$this->getElement('hosts')->setValue($this->host);
}
protected function addHostOverrideCheckbox()
{
$this->addElement('checkbox', 'host_override', [
'ignore' => true,
'class' => 'autosubmit',
'value' => ! empty($this->parent->getStateOverrides($this->node->getName())),
'label' => $this->translate('Override Host State'),
'description' => $this->translate('Enable host state overrides')
]);
}
protected function addHostOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'states' => $this->enumHostStateList(),
'value' => $this->parent->getStateOverrides($this->node->getName()),
'label' => $this->translate('State Overrides')
]);
}
protected function addServicesElement($host)
{
$this->addElement('select', 'children', array(
'required' => true,
'value' => $this->getNode()->getName(),
'multiOptions' => $this->enumServiceList($host),
'label' => $this->translate('Service'),
'description' => $this->translate('The service for this business process node'),
'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]]
));
}
protected function addServiceOverrideCheckbox()
{
$this->addElement('checkbox', 'service_override', [
'ignore' => true,
'class' => 'autosubmit',
'value' => ! empty($this->parent->getStateOverrides($this->node->getName())),
'label' => $this->translate('Override Service State'),
'description' => $this->translate('Enable service state overrides')
]);
}
protected function addServiceOverrideElement()
{
$this->addElement('stateOverrides', 'stateOverrides', [
'required' => true,
'states' => $this->enumServiceStateList(),
'value' => $this->parent->getStateOverrides($this->node->getName()),
'label' => $this->translate('State Overrides')
]);
}
protected function selectProcess()
{
$this->addElement('multiselect', 'children', array(
'label' => $this->translate('Process nodes'),
'required' => true,
'size' => 8,
'style' => 'width: 25em',
'multiOptions' => $this->enumProcesses(),
'description' => $this->translate(
'Other processes that should be part of this business process node'
)
));
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param BpNode|null $node
* @return $this
*/
public function setParentNode(BpNode $node = null)
{
$this->parent = $node;
return $this;
}
/**
* @return bool
*/
public function hasParentNode()
{
return $this->parent !== null;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
protected function hasProcesses()
{
return count($this->enumProcesses()) > 0;
}
protected function enumProcesses()
{
$list = array();
$parents = array();
if ($this->hasParentNode()) {
$this->collectAllParents($this->parent, $parents);
$parents[$this->parent->getName()] = $this->parent;
}
foreach ($this->bp->getNodes() as $node) {
if ($node instanceof BpNode && ! isset($parents[$node->getName()])) {
$list[$node->getName()] = $node->getName(); // display name?
}
}
if (! $this->bp->getMetadata()->isManuallyOrdered()) {
natcasesort($list);
}
return $list;
}
/**
* Collect the given node's parents recursively into the given array by their names
* Set the affected configuration
*
* @param BpNode $node
* @param BpNode[] $parents
* @param BpConfig $bp
*
* @return $this
*/
protected function collectAllParents(BpNode $node, array &$parents)
public function setProcess(BpConfig $bp): self
{
foreach ($node->getParents() as $parent) {
$parents[$parent->getName()] = $parent;
$this->collectAllParents($parent, $parents);
}
$this->bp = $bp;
return $this;
}
/**
* Set the affected node
*
* @param Node $node
*
* @return $this
*/
public function setNode(Node $node)
public function setNode(Node $node): self
{
$this->node = $node;
$this->populate([
'node-search' => $node->getName(),
'node-label' => $node->getAlias()
]);
return $this;
}
public function getNode()
/**
* Set the affected sub-process
*
* @param ?BpNode $node
*
* @return $this
*/
public function setParentNode(BpNode $node = null): self
{
return $this->node;
$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;
}
public function onSuccess()
/**
* 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);
switch ($this->getValue('node_type')) {
case 'host':
case 'service':
$stateOverrides = $this->getValue('stateOverrides') ?: [];
if (! empty($stateOverrides)) {
$stateOverrides = array_merge(
$this->parent->getStateOverrides(),
[$this->getValue('children') => $stateOverrides]
);
} else {
$stateOverrides = $this->parent->getStateOverrides();
unset($stateOverrides[$this->getValue('children')]);
}
$changes->modifyNode($this->parent, ['stateOverrides' => $stateOverrides]);
// Fallthrough
case 'process':
$changes->addChildrenToNode($this->getValue('children'), $this->parent);
break;
case 'new-process':
$properties = $this->getValues();
unset($properties['name']);
if ($this->hasParentNode()) {
$properties['parentName'] = $this->parent->getName();
}
$changes->createNode($this->getValue('name'), $properties);
break;
$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()
);
}
// 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();
}
public function isValid($data)
{
// Don't allow to override disabled elements. This is probably too harsh
// but also wouldn't be necessary if this would be a Icinga\Web\Form...
foreach ($this->getElements() as $element) {
/** @var \Zend_Form_Element $element */
if ($element->getAttrib('disabled')) {
$data[$element->getName()] = $element->getValue();
}
}
return parent::isValid($data);
}
}

View file

@ -3,18 +3,19 @@
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\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Web\Session;
use Icinga\Web\Session\SessionNamespace;
class MoveNodeForm extends QuickForm
class MoveNodeForm extends BpConfigBaseForm
{
/** @var BpConfig */
protected $bp;
@ -95,16 +96,6 @@ class MoveNodeForm extends QuickForm
$this->setSubmitLabel('movenode');
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
return $this;
}
/**
* @param Node $node
* @return $this
@ -125,16 +116,6 @@ class MoveNodeForm extends QuickForm
return $this;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
public function onSuccess()
{
if (! CsrfToken::isValid($this->getValue('csrfToken'))) {
@ -156,7 +137,9 @@ class MoveNodeForm extends QuickForm
);
} catch (ModificationError $e) {
$this->notifyError($e->getMessage());
Icinga::app()->getResponse()
/** @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)
@ -171,7 +154,11 @@ class MoveNodeForm extends QuickForm
$this->notifySuccess($this->getSuccessMessage($this->translate('Node order updated')));
$response = $this->getRequest()->getResponse()
->setHeader('X-Icinga-Container', 'ignore');
->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();

View file

@ -3,50 +3,38 @@
namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Modification\ProcessChanges;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Web\Notification;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
use Icinga\Web\View;
class ProcessForm extends QuickForm
class ProcessForm extends BpConfigBaseForm
{
/** @var MonitoringBackend|IcingaDbConnection */
protected $backend;
/** @var BpConfig */
protected $bp;
/** @var BpNode */
protected $node;
protected $objectList = array();
protected $processList = array();
/** @var SessionNamespace */
protected $session;
public function setup()
{
if ($this->node === null) {
$this->addElement('text', 'name', array(
'label' => $this->translate('ID'),
'required' => true,
'description' => $this->translate(
'This is the unique identifier of this process'
),
));
} else {
if ($this->node !== null) {
/** @var View $view */
$view = $this->getView();
$this->addHtml(
'<h2>' . $this->getView()->escape(
'<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(
@ -58,21 +46,7 @@ class ProcessForm extends QuickForm
$this->addElement('select', 'operator', array(
'label' => $this->translate('Operator'),
'required' => true,
'multiOptions' => array(
'&' => $this->translate('AND'),
'|' => $this->translate('OR'),
'!' => $this->translate('NOT'),
'%' => $this->translate('DEGRADED'),
'1' => $this->translate('MIN 1'),
'2' => $this->translate('MIN 2'),
'3' => $this->translate('MIN 3'),
'4' => $this->translate('MIN 4'),
'5' => $this->translate('MIN 5'),
'6' => $this->translate('MIN 6'),
'7' => $this->translate('MIN 7'),
'8' => $this->translate('MIN 8'),
'9' => $this->translate('MIN 9'),
)
'multiOptions' => Node::getOperators()
));
if ($this->node !== null) {
@ -111,27 +85,6 @@ class ProcessForm extends QuickForm
}
}
/**
* @param MonitoringBackend|IcingaDbConnection $backend
* @return $this
*/
public function setBackend($backend)
{
$this->backend = $backend;
return $this;
}
/**
* @param BpConfig $process
* @return $this
*/
public function setProcess(BpConfig $process)
{
$this->bp = $process;
$this->setBackend($process->getBackend());
return $this;
}
/**
* @param BpNode $node
* @return $this
@ -142,16 +95,6 @@ class ProcessForm extends QuickForm
return $this;
}
/**
* @param SessionNamespace $session
* @return $this
*/
public function setSession(SessionNamespace $session)
{
$this->session = $session;
return $this;
}
public function onSuccess()
{
$changes = ProcessChanges::construct($this->bp, $this->session);

View file

@ -4,14 +4,15 @@ namespace Icinga\Module\Businessprocess\Forms;
use Icinga\Module\Businessprocess\MonitoredNode;
use Icinga\Module\Businessprocess\Simulation;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
use Icinga\Web\View;
class SimulationForm extends QuickForm
class SimulationForm extends BpConfigBaseForm
{
/** @var MonitoredNode */
protected $node;
/** @var MonitoredNode */
/** @var ?MonitoredNode */
protected $simulatedNode;
/** @var Simulation */
@ -36,6 +37,7 @@ class SimulationForm extends QuickForm
$node = $this->node;
}
/** @var View $view */
$view = $this->getView();
if ($hasSimulation) {
$title = $this->translate('Modify simulation for %s');
@ -44,7 +46,7 @@ class SimulationForm extends QuickForm
}
$this->addHtml(
'<h2>'
. $view->escape(sprintf($title, $node->getAlias()))
. $view->escape(sprintf($title, $node->getAlias() ?? $node->getName()))
. '</h2>'
);

View file

@ -1,40 +0,0 @@
<?php
// Avoid complaints about missing namespace and invalid class name
// @codingStandardsIgnoreStart
class Zend_View_Helper_FormStateOverrides extends Zend_View_Helper_FormElement
{
// @codingStandardsIgnoreEnd
public function formStateOverrides($name, $value = null, $attribs = null)
{
$states = $attribs['states'];
unset($attribs['states']);
$attribs['multiple'] = '';
$html = '';
foreach ($states as $state => $label) {
if ($state === 0) {
continue;
}
$chosen = $state;
if (isset($value[$state])) {
$chosen = $value[$state];
}
$options = [$state => t('Keep actual state')] + $states;
$html .= '<label><span>' . $this->view->escape($label) . '</span>';
$html .= $this->view->formSelect(
sprintf('%s[%d]', substr($name, 0, -2), $state),
$chosen,
$attribs,
$options
);
$html .= '</label>';
}
return $html;
}
}

View file

@ -1,10 +1,9 @@
# Icinga Business Process Modelling
# Icinga Business Process Modeling
If you want to visualize and monitor hierarchical business processes based on
any or all objects monitored by Icinga, the Icinga Web 2 business process
module is the way to go.
objects monitored by Icinga, Icinga Business Process Modeling is the solution.
[![Dashboard](screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png)](doc/16-Add-To-Dashboard.md)
[![Dashboard](screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png)](16-Add-To-Dashboard.md)
Want to create custom process-based dashboards? Trigger notifications at
process or sub-process level? Provide a quick top-level view for thousands of

View file

@ -1,20 +1,24 @@
# Installation
<!-- {% if index %} -->
# Installing Icinga Business Process Modeling
## Requirements
The recommended way to install Icinga Business Process Modeling is to use prebuilt packages for
all supported platforms from our official release repository.
Please note that [Icinga Web](https://icinga.com/docs/icinga-web) is required to run Icinga
Business Process Modeling and if it is not already set up, it is best to do this first.
* PHP (>= 7.2)
* Icinga Web 2 (>= 2.9)
* Icinga Web 2 libraries:
* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (>= 0.8)
* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (>= 0.11)
* Icinga Web 2 modules:
* The `monitoring` or `icingadb` module needs to be configured and enabled.
The following steps will guide you through installing and setting up Icinga Business Process Modeling.
<!-- {% else %} -->
<!-- {% if not icingaDocs %} -->
## Install Icinga Business Process Modeling
## Installing the Package
Install it [like any other module](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation).
Use `businessprocess` as name.
If the [repository](https://packages.icinga.com) is not configured yet, please add it first.
Then use your distribution's package manager to install the `icinga-businessprocess` package
or install [from source](02-Installation.md.d/From-Source.md).
<!-- {% endif %} --><!-- {# end if not icingaDocs #} -->
## Create your first Business Process definition
## Configuring Icinga Business Process Modeling
That's it, *Business Process* is now ready for use. Please read more on [how to get started](03-Getting-Started.md).
That's it, Icinga Business Process Modeling is now ready to use.
Please read more on [how to get started](03-Getting-Started.md).
<!-- {% endif %} --><!-- {# end else if index #} -->

View file

@ -0,0 +1,15 @@
# Installing Icinga Business Process Modeling from Source
Please see the Icinga Web documentation on
[how to install modules](https://icinga.com/docs/icinga-web/latest/doc/08-Modules/#installation) from source.
Make sure you use `businessprocess` as the module name. The following requirements must also be met.
## Requirements
* PHP (≥7.2)
* [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9)
* [Icinga DB Web](https://github.com/Icinga/icingadb-web) (≥1.0)
* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.13.0)
* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥0.12.0)
<!-- {% include "02-Installation.md" %} -->

View file

@ -1,13 +1,11 @@
<a id="Getting-Started"></a>Getting Started
===========================================
# Getting Started
Once you enable the *Business Process* module, it will pop up in your menu.
When you click on it, it will show you a new Dashboard:
Once you enable Icinga Business Process Modeling, it will pop up in your menu.
If you click on it, it will show you a new Dashboard:
![Empty Dashboard](screenshot/03_getting-started/0201_empty-dashboard.png)
A new Business Process configuration
-------------------------------------------
## A new Business Process configuration
From here we choose to create a new *Business Process configuration*:
@ -65,8 +63,7 @@ first five configurations a user is allowed to see will be shown there:
That's all for now, click `Add` to store your new (still empty) Business Process
configuration.
Empty configuration
===================
## Empty configuration
You are redirected to your newly created Business Process configuration:

View file

@ -1,5 +1,4 @@
<a id="Create-your-first-process-node"></a>Create your first Business Process Node
==================================================================================
# Create your first Business Process Node
A *Business Process Node* consists of a *name*, *title*, an *operator* and one or
more child nodes. It can be a Root Node, child node of other Business Process
@ -7,8 +6,7 @@ Nodes - or both.
![Empty Config](screenshot/04_first-root-node/0301_empty-config.png)
Configuring our first node
--------------------------
## Configuring our first node
To create our first *Business Process Node* we click the *Add* button. This
leads to the related configuration form:

View file

@ -1,5 +1,4 @@
<a id="Importing-Processes"></a>Importing Processes
===================================================
# Importing Processes
To avoid redundancy and make complex *Business Process Configurations* easier
to maintain it is possible to import processes from other configurations.
@ -9,8 +8,7 @@ import processes into the root level.
![Subprocesses Only](screenshot/05_importing_nodes/0401_subprocesses_only.png)
Importing a Process
-------------------
## Importing a Process
Once the related configuration form is open, choose `Existing Process` and wait
for the form to refresh.
@ -37,8 +35,7 @@ to save your changes!
![Import Successful](screenshot/05_importing_nodes/0405_import_successful.png)
Navigation with Imported Processes
----------------------------------
## Navigation with Imported Processes
### Seamless Breadcrumbs

View file

@ -1,5 +1,4 @@
<a id="Customize-Node-Order"></a>Customize Node Order
=====================================================
# Customize Node Order
By default all nodes are ordered alphabetically while viewing them in the UI.
Though, it is also possible to order nodes entirely manually.
@ -9,8 +8,7 @@ Though, it is also possible to order nodes entirely manually.
> Once manual order is applied (no matter where) alphabetical order is
> disabled for the entire configuration.
Reorder by Drag'n'Drop
----------------------
## Reorder by Drag'n'Drop
Make sure to unlock the configuration first to be able to reorder nodes.
@ -34,8 +32,7 @@ The tree view also has an advantage the tile view has not. It is possible to
move nodes within the entire hierarchy. But remember to unfold processes first,
if you want to move a node into them.
File Format Extensions
----------------------
## File Format Extensions
The configuration file format has slightly been changed to accommodate the new
manual order. Though, previous configurations are perfectly upwards compatible.

View file

@ -1,15 +1,15 @@
# Operators <a id="operators">
# Operators
Every Business Process requires an Operator. This operator defines its behaviour and specifies how its very own state is
going to be calculated.
## AND <a id="and-operator">
## AND
The `AND` operator selects the **WORST** state of its child nodes:
![And Operator](screenshot/09_operators/0901_and-operator.png)
## OR <a id="or-operator">
## OR
The `OR` operator selects the **BEST** state of its child nodes:
@ -17,7 +17,17 @@ The `OR` operator selects the **BEST** state of its child nodes:
![Or Operator #2](screenshot/09_operators/0903_or-operator-without-ok.png)
## DEGRADED <a id="deg-operator">
## XOR
The `XOR` operator shows OK if only one of n children is OK at the same time. In all other cases the parent node is CRITICAL.
Useful for a service on n servers, only one of which may be running. If both were running,
race conditions and duplication of data could occur.
![Xor Operator](screenshot/09_operators/0906_xor-operator.png)
![Xor Operator #2](screenshot/09_operators/0907_xor-operator-not-ok.png)
## DEGRADED
The `DEGRADED` operator behaves like an `AND`, but if the resulting
state is **CRITICAL** it transforms it into a **WARNING**.
@ -26,7 +36,7 @@ analysis of the statuses.
![Degraded Operator](screenshot/09_operators/0905_deg-operator.jpg)
## MIN n <a id="min-operator">
## MIN n
The `MIN` operator selects the **WORST** state out of the **BEST n** child node states:

View file

@ -1,5 +1,4 @@
<a id="Web-Components-Breadcrumb"></a>Web Components: Breadcrumb
================================================================
# Web Components: Breadcrumb
All Business Process renderers show a **breadcrumb** component to always give
you a quick indication of your current location.
@ -24,8 +23,7 @@ column view to make it obvious that you moved to another context. It is also
perfectly legal to open any of the available links in a new browser tab or
window.
Available actions below the Breadcrumb
--------------------------------------
## Available actions below the Breadcrumb
### Choose a renderer
@ -60,8 +58,7 @@ settings for the your currently loaded *Business Process Configuration*:
But there is more. When unlocked, all nodes provide links allowing you to modify or
to delete them. Host/Service Nodes now allow you to simulate a specific state.
Other main actions
------------------
## Other main actions
### Add content to your Dashboard

View file

@ -1,5 +1,4 @@
<a id="Web-Components-Tile-Renderer"></a>Web Components: Tile Renderer
======================================================================
# Web Components: Tile Renderer
The default Business Process *Renderer* is the *Tile Renderer*. It always shows
one level of your tree, enriched with badges giving some hint on lower level

View file

@ -1,5 +1,4 @@
<a id="Web-Components-Tree-Renderer"></a>Web Components: Tree Renderer
======================================================================
# Web Components: Tree Renderer
The main advantage of the *Tree Renderer* is that it is able to show all nodes
of Business Process trees at once. This works fine even for huge trees with lots

View file

@ -1,5 +1,4 @@
<a id="Add-To-Dashboard"></a>Show Processes on a Dashboard
==========================================================
# Show Processes on a Dashboard
When being in *Locked mode*, you can add any Business Process at top or sub level
to any Icinga Web 2 Dashboard. The related link can be found in the Tab bar:
@ -13,8 +12,7 @@ want to create a dedicated Dashboard as shown in this example:
![Add to Dashboard - Form](screenshot/16_dashboard/1602_add_to_dashboard-form.png)
Want more?
----------
## Want more?
Head on and add multiple Business Processes to your Dashboard to show all of
them at once:

View file

@ -1,5 +1,4 @@
<a id="Store-Config"></a>Store your Configuration
=================================================
# Store your Configuration
Changes to your *Business Process Configuration* are added to a stack and will
not be stored immediately. In case there are pending unstored changes, this will
@ -13,8 +12,7 @@ you created your [very first configuration](03-Getting-Started.md):
![Store Config](screenshot/21_store-config/2102_Store-Config.png)
Config Diff
-----------
## Config Diff
If unsure what changes you're going to store, you can still check the *Config Diff*
before finally storing to disk:

View file

@ -1,5 +1,4 @@
<a id="Upload-Config"></a>Upload a Configuration File
=====================================================
# Upload a Configuration File
You can upload a formerly downloaded or even a manually created file directly
through the web frontend. Given sufficient permissions, the Dashboard provides
@ -7,14 +6,13 @@ a related link:
![From Dashboard to Upload](screenshot/22_upload-config/2201_go-to-upload.png)
Chose a file
------------
## Chose a file
This can be any file:
![Choose a File](screenshot/22_upload-config/2202_choose-file.png)
It should be valid of course, but don't worry - the *Business Process* module
It should be valid of course, but don't worry - Icinga Business Process Modeling
protects you from syntax errors:
![Syntax Error](screenshot/22_upload-config/2203_syntax-error.png)

View file

@ -1,10 +1,8 @@
<a id="Permission System"></a>Permission System
=================================================
# Permission System
The permission system of the module is based on permissions and restrictions.
Permissions
-----------
## Permissions
The module has five levels of permissions:
@ -14,8 +12,7 @@ The module has five levels of permissions:
* Permission to view all business processes regardless restrictions. (`businessprocess/showall`)
* Full permissions. (`businessprocess/*`)
Restrictions
-----------
## Restrictions
There are two ways to configure restrictions: prefix-based and access controls

View file

@ -1,15 +1,13 @@
Project History
===============
# Project History
The Business Process module is based on the ideas of the Nagios(tm) [Business
Icinga Business Process Modeling is based on the ideas of the Nagios(tm) [Business
Process AddOn](http://bp-addon.monitoringexchange.org/) written by Bernd
Strößenreuther. We always loved its simplicity, and while it looks pretty
oldschool right now there are still many shops happily using it in production.
![BpAddOn Overview](screenshot/81_history/8101_bpaddon-overview.png)
Compatibility
-------------
## Compatibility
We fully support the BPaddon configuration language and will continue to do so.
It's also perfectly valid to run both products in parallel based on the very same
@ -33,8 +31,7 @@ backends like SQL databases or the Icinga 2 DSL.
This would make it easier to distribute configuration in large environments.
Improvements
------------
## Improvements
Major focus has been put on execution speed. So while the Web integration shows
much more details at once and is able to display huge unfolded trees, it should

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -130,6 +130,9 @@ class BpConfig
/** @var ProcessChanges */
protected $appliedChanges;
/** @var bool Whether the config is faulty */
protected $isFaulty = false;
public function __construct()
{
}
@ -223,7 +226,7 @@ class BpConfig
/**
* Whether changes have been applied to this configuration
*
* @return int
* @return bool
*/
public function hasChanges()
{
@ -432,33 +435,12 @@ class BpConfig
*/
public function getRootNodes()
{
if ($this->getMetadata()->isManuallyOrdered()) {
uasort($this->root_nodes, function (BpNode $a, BpNode $b) {
$a = $a->getDisplay();
$b = $b->getDisplay();
return $a > $b ? 1 : ($a < $b ? -1 : 0);
});
} else {
ksort($this->root_nodes, SORT_NATURAL | SORT_FLAG_CASE);
}
return $this->root_nodes;
}
public function listRootNodes()
{
$names = array_keys($this->root_nodes);
if ($this->getMetadata()->isManuallyOrdered()) {
uasort($names, function ($a, $b) {
$a = $this->root_nodes[$a]->getDisplay();
$b = $this->root_nodes[$b]->getDisplay();
return $a > $b ? 1 : ($a < $b ? -1 : 0);
});
} else {
natcasesort($names);
}
return $names;
return array_keys($this->root_nodes);
}
public function getNodes()
@ -492,7 +474,7 @@ class BpConfig
)
);
$node->setBpConfig($this);
$this->nodes[$host . ';' . $service] = $node;
$this->nodes[$node->getName()] = $node;
$this->hosts[$host] = true;
return $node;
}
@ -501,7 +483,7 @@ class BpConfig
{
$node = new HostNode((object) array('hostname' => $host));
$node->setBpConfig($this);
$this->nodes[$host . ';Hoststatus'] = $node;
$this->nodes[$node->getName()] = $node;
$this->hosts[$host] = true;
return $node;
}
@ -598,7 +580,13 @@ class BpConfig
public function getImportedConfig($name)
{
if (! isset($this->importedConfigs[$name])) {
$import = $this->storage()->loadProcess($name);
try {
$import = $this->storage()->loadProcess($name);
} catch (Exception $e) {
$import = (new static())
->setName($name)
->setFaulty();
}
if ($this->usesSoftStates()) {
$import->useSoftStates();
@ -643,7 +631,7 @@ class BpConfig
/**
* @param string $name
* @return Node
* @return MonitoredNode|BpNode
* @throws Exception
*/
public function getNode($name)
@ -663,15 +651,13 @@ class BpConfig
// Fallback: if it is a service, create an empty one:
$this->warn(sprintf('The node "%s" doesn\'t exist', $name));
$pos = strpos($name, ';');
if ($pos !== false) {
$host = substr($name, 0, $pos);
$service = substr($name, $pos + 1);
// TODO: deactivated, this scares me, test it
if ($service === 'Hoststatus') {
return $this->createHost($host);
[$name, $suffix] = self::splitNodeName($name);
if ($suffix !== null) {
if ($suffix === 'Hoststatus') {
return $this->createHost($name);
} else {
return $this->createService($host, $service);
return $this->createService($name, $suffix);
}
}
@ -710,9 +696,17 @@ class BpConfig
{
if ($this->hasBpNode($name)) {
return $this->nodes[$name];
} else {
throw new NotFoundError('Trying to access a missing business process node "%s"', $name);
}
$msg = $this->isFaulty()
? sprintf(
t('Trying to import node "%s" from faulty config file "%s.conf"'),
self::unescapeName($name),
$this->getName()
)
: sprintf(t('Trying to access a missing business process node "%s"'), $name);
throw new NotFoundError($msg);
}
/**
@ -826,16 +820,6 @@ class BpConfig
$nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node);
}
if ($this->getMetadata()->isManuallyOrdered()) {
uasort($nodes, function ($a, $b) {
$a = $this->nodes[$a]->getDisplay();
$b = $this->nodes[$b]->getDisplay();
return $a > $b ? 1 : ($a < $b ? -1 : 0);
});
} else {
natcasesort($nodes);
}
return $nodes;
}
@ -945,7 +929,10 @@ class BpConfig
throw new IcingaException($msg);
}
$this->errors[] = $msg;
if (! in_array($msg, $this->errors)) {
$this->errors[] = $msg;
}
return $this;
}
@ -1046,4 +1033,85 @@ class BpConfig
return $data;
}
/**
* Escape the given node name
*
* @param string $name
*
* @return string
*/
public static function escapeName(string $name): string
{
return preg_replace('/((?<!\\\\);)/', '\\\\$1', $name);
}
/**
* Unescape the given node name
*
* @param string $name
*
* @return string
*/
public static function unescapeName(string $name): string
{
return str_replace('\\;', ';', $name);
}
/**
* Join the given two name parts together
*
* The used separator is the semicolon. If a semicolon exists in either part, it's escaped.
*
* @param string $name
* @param ?string $suffix
*
* @return string
*/
public static function joinNodeName(string $name, ?string $suffix = null): string
{
return self::escapeName($name) . ($suffix ? ";$suffix" : '');
}
/**
* Split the given node name into two parts
*
* The first part is always a string, with any semicolons unescaped.
* The second part may be null or a string otherwise.
*
* @param string $nodeName
*
* @return array
*/
public static function splitNodeName(string $nodeName): array
{
$parts = preg_split('/(?<!\\\\);/', $nodeName, 2);
$parts[0] = self::unescapeName($parts[0]);
return array_pad($parts, 2, null);
}
/**
* Set whether the config is faulty
*
* @param bool $isFaulty
*
* @return $this
*/
public function setFaulty(bool $isFaulty = true): self
{
$this->isFaulty = $isFaulty;
return $this;
}
/**
* Get whether the config is faulty
*
* @return bool
*/
public function isFaulty(): bool
{
return $this->isFaulty;
}
}

View file

@ -5,20 +5,23 @@ namespace Icinga\Module\Businessprocess;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Businessprocess\Exception\NestingError;
use ipl\Web\Widget\Icon;
class BpNode extends Node
{
const OP_AND = '&';
const OP_OR = '|';
const OP_XOR = '^';
const OP_NOT = '!';
const OP_DEGRADED = '%';
protected $operator = '&';
protected $url;
protected $info_command;
protected $display = 0;
/** @var Node[] */
/** @var ?Node[] */
protected $children;
/** @var array */
@ -54,7 +57,8 @@ class BpNode extends Node
public function __construct($object)
{
$this->name = $object->name;
$this->name = BpConfig::escapeName($object->name);
$this->alias = BpConfig::unescapeName($object->name);
$this->operator = $object->operator;
$this->childNames = $object->child_names;
}
@ -133,7 +137,6 @@ class BpNode extends Node
$this->children[$name] = $node;
$this->childNames[] = $name;
$this->reorderChildren();
$node->addParent($this);
return $this;
}
@ -170,6 +173,8 @@ class BpNode extends Node
if (! empty($this->children)) {
unset($this->children[$name]);
}
$this->childNames = array_values($this->childNames);
}
return $this;
@ -272,11 +277,11 @@ class BpNode extends Node
foreach ($this->getChildren() as $child) {
if ($child->isMissing()) {
$missing[$child->getName()] = $child;
$missing[$child->getAlias() ?? $child->getName()] = $child;
}
foreach ($child->getMissingChildren() as $m) {
$missing[$m->getName()] = $m;
$missing[$m->getAlias() ?? $m->getName()] = $m;
}
}
@ -303,6 +308,7 @@ class BpNode extends Node
switch ($operator) {
case self::OP_AND:
case self::OP_OR:
case self::OP_XOR:
case self::OP_NOT:
case self::OP_DEGRADED:
return;
@ -334,21 +340,6 @@ class BpNode extends Node
return $this->url;
}
public function setInfoCommand($cmd)
{
$this->info_command = $cmd;
}
public function hasInfoCommand()
{
return $this->info_command !== null;
}
public function getInfoCommand()
{
return $this->info_command;
}
public function setStateOverrides(array $overrides, $name = null)
{
if ($name === null) {
@ -476,6 +467,21 @@ class BpNode extends Node
case self::OP_OR:
$sort_state = min($sort_states);
break;
case self::OP_XOR:
$actualGood = 0;
foreach ($sort_states as $s) {
if ($this->sortStateTostate($s) === self::ICINGA_OK) {
$actualGood++;
}
}
if ($actualGood === 1) {
$this->state = self::ICINGA_OK;
} else {
$this->state = self::ICINGA_CRITICAL;
}
return $this;
case self::OP_DEGRADED:
$maxState = max($sort_states);
$flags = $maxState & 0xf;
@ -546,7 +552,6 @@ class BpNode extends Node
{
$this->childNames = $names;
$this->children = null;
$this->reorderChildren();
return $this;
}
@ -565,7 +570,6 @@ class BpNode extends Node
{
if ($this->children === null) {
$this->children = [];
$this->reorderChildren();
foreach ($this->getChildNames() as $name) {
$this->children[$name] = $this->getBpConfig()->getNode($name);
$this->children[$name]->addParent($this);
@ -575,29 +579,6 @@ class BpNode extends Node
return $this->children;
}
/**
* Reorder this node's children, in case manual order is not applied
*/
protected function reorderChildren()
{
if ($this->getBpConfig()->getMetadata()->isManuallyOrdered()) {
return;
}
$childNames = $this->getChildNames();
natcasesort($childNames);
$this->childNames = array_values($childNames);
if (! empty($this->children)) {
$children = [];
foreach ($this->childNames as $name) {
$children[$name] = $this->children[$name];
}
$this->children = $children;
}
}
/**
* return BpNode[]
*/
@ -642,16 +623,14 @@ class BpNode extends Node
switch ($this->getOperator()) {
case self::OP_AND:
return 'AND';
break;
case self::OP_OR:
return 'OR';
break;
case self::OP_XOR:
return 'XOR';
case self::OP_NOT:
return 'NOT';
break;
case self::OP_DEGRADED:
return 'DEG';
break;
default:
// MIN
$this->assertNumericOperator();
@ -659,7 +638,7 @@ class BpNode extends Node
}
}
public function getIcon()
public function getIcon(): Icon
{
$this->icon = $this->hasParents() ? 'cubes' : 'sitemap';
return parent::getIcon();

View file

@ -1,169 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess\Common;
use Icinga\Application\Modules\Module;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Businessprocess\IcingaDbObject;
use Icinga\Module\Businessprocess\MonitoringRestrictions;
use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
trait EnumList
{
protected function enumHostForServiceList()
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldHostnames();
} else {
$names = $this->backend
->select()
->from('hostStatus', ['hostname' => 'host_name'])
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('host_name')
->getQuery()
->fetchColumn();
}
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
foreach ($names as $name) {
$res[$name] = $name;
}
return $res;
}
protected function enumHostList()
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldHostnames();
} else {
$names = $this->backend
->select()
->from('hostStatus', ['hostname' => 'host_name'])
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('host_name')
->getQuery()
->fetchColumn();
}
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
$suffix = ';Hoststatus';
foreach ($names as $name) {
$res[$name . $suffix] = $name;
}
return $res;
}
protected function enumServiceList($host)
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldServicenames($host);
} else {
$names = $this->backend
->select()
->from('serviceStatus', ['service' => 'service_description'])
->where('host_name', $host)
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('service_description')
->getQuery()
->fetchColumn();
}
$services = array();
foreach ($names as $name) {
$services[$host . ';' . $name] = $name;
}
return $services;
}
protected function enumHostListByFilter($filter)
{
if ($this->useIcingaDbBackend()) {
$names = (new IcingaDbObject())->yieldHostnames($filter);
} else {
$names = $this->backend
->select()
->from('hostStatus', ['hostname' => 'host_name'])
->applyFilter(Filter::fromQueryString($filter))
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('host_name')
->getQuery()
->fetchColumn();
}
// fetchPairs doesn't seem to work when using the same column with
// different aliases twice
$res = array();
$suffix = ';Hoststatus';
foreach ($names as $name) {
$res[$name . $suffix] = $name;
}
return $res;
}
protected function enumServiceListByFilter($filter)
{
$services = array();
if ($this->useIcingaDbBackend()) {
$objects = (new IcingaDbObject())->fetchServices($filter);
foreach ($objects as $object) {
$services[$object->host->name . ';' . $object->name] = $object->host->name . ':' . $object->name;
}
} else {
$objects = $this->backend
->select()
->from('serviceStatus', ['host' => 'host_name', 'service' => 'service_description'])
->applyFilter(Filter::fromQueryString($filter))
->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
->order('service_description')
->getQuery()
->fetchAll();
foreach ($objects as $object) {
$services[$object->host . ';' . $object->service] = $object->host . ':' . $object->service;
}
}
return $services;
}
protected function enumHostStateList()
{
$hostStateList = [
0 => $this->translate('UP'),
1 => $this->translate('DOWN'),
99 => $this->translate('PENDING')
];
return $hostStateList;
}
protected function enumServiceStateList()
{
$serviceStateList = [
0 => $this->translate('OK'),
1 => $this->translate('WARNING'),
2 => $this->translate('CRITICAL'),
3 => $this->translate('UNKNOWN'),
99 => $this->translate('PENDING'),
];
return $serviceStateList;
}
protected function useIcingaDbBackend()
{
if (Module::exists('icingadb')) {
return ! $this->bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend();
}
return false;
}
}

View file

@ -0,0 +1,158 @@
<?php
// Icinga Business Process Modelling | (c) 2023 Icinga GmbH | GPLv2
namespace Icinga\Module\Businessprocess\Common;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Node;
use InvalidArgumentException;
use ipl\Stdlib\Str;
trait Sort
{
/** @var ?string Current sort specification */
protected $sort;
/** @var ?callable Actual sorting function */
protected $sortFn;
/**
* Get the sort specification
*
* @return ?string
*/
public function getSort(): ?string
{
return $this->sort;
}
/**
* Set the sort specification
*
* @param ?string $sort
*
* @return $this
*
* @throws InvalidArgumentException When sorting according to the specified specification is not possible
*/
public function setSort(?string $sort): self
{
if (empty($sort)) {
return $this;
}
list($sortBy, $direction) = Str::symmetricSplit($sort, ' ', 2, 'asc');
switch ($sortBy) {
case 'manual':
if ($direction === 'asc') {
$this->sortFn = function (array &$nodes) {
$firstNode = reset($nodes);
if ($firstNode instanceof BpNode && $firstNode->getDisplay() > 0) {
$nodes = self::applyManualSorting($nodes);
}
// Child nodes don't need to be ordered in this case, their implicit order is significant
};
} else {
$this->sortFn = function (array &$nodes) {
$firstNode = reset($nodes);
if ($firstNode instanceof BpNode && $firstNode->getDisplay() > 0) {
uasort($nodes, function (BpNode $a, BpNode $b) {
return $b->getDisplay() <=> $a->getDisplay();
});
} else {
$nodes = array_reverse($nodes);
}
};
}
break;
case 'display_name':
if ($direction === 'asc') {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return strnatcasecmp(
$a->getAlias() ?? $a->getName(),
$b->getAlias() ?? $b->getName()
);
});
};
} else {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return strnatcasecmp(
$b->getAlias() ?? $b->getName(),
$a->getAlias() ?? $a->getName()
);
});
};
}
break;
case 'state':
if ($direction === 'asc') {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return $a->getSortingState() <=> $b->getSortingState();
});
};
} else {
$this->sortFn = function (array &$nodes) {
uasort($nodes, function (Node $a, Node $b) {
return $b->getSortingState() <=> $a->getSortingState();
});
};
}
break;
default:
throw new InvalidArgumentException(sprintf(
"Can't sort by %s. It's only possible to sort by manual order, display_name or state",
$sortBy
));
}
$this->sort = $sort;
return $this;
}
/**
* Sort the given nodes as specified by {@see setSort()}
*
* If {@see setSort()} has not been called yet, the default sort specification is used
*
* @param array $nodes
*
* @return array
*/
public function sort(array $nodes): array
{
if (empty($nodes)) {
return $nodes;
}
if ($this->sortFn !== null) {
call_user_func_array($this->sortFn, [&$nodes]);
}
return $nodes;
}
/**
* Apply manual sort order on the given process nodes
*
* @param array $bpNodes
*
* @return array
*/
public static function applyManualSorting(array $bpNodes): array
{
uasort($bpNodes, function (BpNode $a, BpNode $b) {
return $a->getDisplay() <=> $b->getDisplay();
});
return $bpNodes;
}
}

View file

@ -1,36 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess;
use Icinga\Web\Request;
use Icinga\Web\Form as WebForm;
class Form extends WebForm
{
public function __construct($options = null)
{
parent::__construct($options);
$this->setup();
}
public function addHidden($name, $value = null)
{
$this->addElement('hidden', $name);
$this->getElement($name)->setDecorators(array('ViewHelper'));
if ($value !== null) {
$this->setDefault($name, $value);
}
return $this;
}
public function handleRequest(Request $request = null)
{
parent::handleRequest();
return $this;
}
public static function construct()
{
return new static;
}
}

View file

@ -31,11 +31,11 @@ class HostNode extends MonitoredNode
protected $className = 'host';
protected $icon = 'host';
protected $icon = 'laptop';
public function __construct($object)
{
$this->name = $object->hostname . ';Hoststatus';
$this->name = BpConfig::joinNodeName($object->hostname, 'Hoststatus');
$this->hostname = $object->hostname;
if (isset($object->state)) {
$this->setState($object->state);

View file

@ -28,7 +28,7 @@ class ImportedNode extends BpNode
{
$this->parentBp = $parentBp;
$this->configName = $object->configName;
$this->nodeName = $object->node;
$this->nodeName = BpConfig::escapeName($object->node);
parent::__construct((object) [
'name' => '@' . $this->configName . ':' . $this->nodeName,
@ -69,11 +69,7 @@ class ImportedNode extends BpNode
public function getAlias()
{
if ($this->alias === null) {
$this->alias = $this->importedNode()->getAlias();
}
return $this->alias;
return $this->importedNode()->getAlias();
}
public function getOperator()
@ -94,6 +90,15 @@ class ImportedNode extends BpNode
return $this->childNames;
}
public function isMissing()
{
if ($this->missing === null && $this->getBpConfig()->isFaulty()) {
$this->missing = true;
}
return parent::isMissing();
}
/**
* @return BpNode
*/
@ -125,10 +130,9 @@ class ImportedNode extends BpNode
));
$node->setBpConfig($this->getBpConfig());
$node->setState(2);
$node->setMissing(false)
$node->setMissing()
->setDowntime(false)
->setAck(false)
->setAlias($e->getMessage());
->setAck(false);
return $node;
}

View file

@ -111,8 +111,8 @@ abstract class NodeAction
public static function create($actionName, $nodeName)
{
$className = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action';
$object = new $className($nodeName);
return $object;
return new $className($nodeName);
}
/**
@ -144,7 +144,7 @@ abstract class NodeAction
*/
public static function unSerialize($string)
{
$object = json_decode($string, JSON_FORCE_OBJECT);
$object = json_decode($string, true);
$action = self::create($object['actionName'], $object['nodeName']);
foreach ($object['properties'] as $key => $val) {

View file

@ -33,13 +33,12 @@ class NodeAddChildrenAction extends NodeAction
foreach ($this->children as $name) {
if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) {
if (strpos($name, ';') !== false) {
list($host, $service) = preg_split('/;/', $name, 2);
if ($service === 'Hoststatus') {
$config->createHost($host);
[$prefix, $suffix] = BpConfig::splitNodeName($name);
if ($suffix !== null) {
if ($suffix === 'Hoststatus') {
$config->createHost($prefix);
} else {
$config->createService($host, $service);
$config->createService($prefix, $suffix);
}
} elseif ($name[0] === '@' && strpos($name, ':') !== false) {
list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);

View file

@ -3,9 +3,12 @@
namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\Sort;
class NodeApplyManualOrderAction extends NodeAction
{
use Sort;
public function appliesTo(BpConfig $config)
{
return $config->getMetadata()->get('ManualOrder') !== 'yes';
@ -20,7 +23,10 @@ class NodeApplyManualOrderAction extends NodeAction
}
if ($node->hasChildren()) {
$node->setChildNames($node->getChildNames());
$node->setChildNames(array_keys(
$this->setSort('display_name asc')
->sort($node->getChildren())
));
}
}

View file

@ -3,9 +3,12 @@
namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\Sort;
class NodeCopyAction extends NodeAction
{
use Sort;
/**
* @param BpConfig $config
* @return bool
@ -31,9 +34,15 @@ class NodeCopyAction extends NodeAction
public function applyTo(BpConfig $config)
{
$name = $this->getNodeName();
$rootNodes = $config->getRootNodes();
$display = 1;
if ($config->getMetadata()->isManuallyOrdered()) {
$rootNodes = self::applyManualSorting($config->getRootNodes());
$display = end($rootNodes)->getDisplay() + 1;
}
$config->addRootNode($name)
->getBpNode($name)
->setDisplay(end($rootNodes)->getDisplay() + 1);
->setDisplay($display);
}
}

View file

@ -4,9 +4,12 @@ namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Common\Sort;
class NodeMoveAction extends NodeAction
{
use Sort;
/**
* @var string
*/
@ -87,16 +90,28 @@ class NodeMoveAction extends NodeAction
$nodes = $parent->getChildNames();
if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
$this->error('Node "%s" not found at position %d', $name, $this->from);
$reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
$this->error('Node "%s" not found at position %d', $name, $this->from);
} else {
$this->from = array_search($reversedNodes[$this->from], $nodes, true);
$this->to = array_search($reversedNodes[$this->to], $nodes, true);
}
}
} else {
if (! $config->hasRootNode($name)) {
$this->error('Toplevel process "%s" not found', $name);
}
$nodes = $config->listRootNodes();
$nodes = array_keys(self::applyManualSorting($config->getRootNodes()));
if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
$this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
$reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
$this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
} else {
$this->from = array_search($reversedNodes[$this->from], $nodes, true);
$this->to = array_search($reversedNodes[$this->to], $nodes, true);
}
}
}
@ -144,7 +159,7 @@ class NodeMoveAction extends NodeAction
if ($this->parent !== null) {
$nodes = $config->getBpNode($this->parent)->getChildren();
} else {
$nodes = $config->getRootNodes();
$nodes = self::applyManualSorting($config->getRootNodes());
}
$node = $nodes[$name];
@ -162,7 +177,7 @@ class NodeMoveAction extends NodeAction
if ($this->newParent !== null) {
$newNodes = $config->getBpNode($this->newParent)->getChildren();
} else {
$newNodes = $config->getRootNodes();
$newNodes = self::applyManualSorting($config->getRootNodes());
}
$newNodes = array_merge(

View file

@ -3,6 +3,8 @@
namespace Icinga\Module\Businessprocess\Modification;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Node;
/**
* NodeRemoveAction
@ -64,6 +66,12 @@ class NodeRemoveAction extends NodeAction
{
$name = $this->getNodeName();
$parentName = $this->getParentName();
$node = $config->getNode($name);
/** @var ?BpNode $parentBpNode */
$parentBpNode = $parentName ? $config->getNode($parentName) : null;
$this->updateStateOverrides($node, $parentBpNode);
if ($parentName === null) {
if (! $config->hasBpNode($name)) {
$config->removeNode($name);
@ -82,7 +90,6 @@ class NodeRemoveAction extends NodeAction
}
}
} else {
$node = $config->getNode($name);
$parent = $config->getBpNode($parentName);
$parent->removeChild($name);
$node->removeParent($parentName);
@ -91,4 +98,28 @@ class NodeRemoveAction extends NodeAction
}
}
}
/**
* Update state overrides
*
* @param Node $node
* @param BpNode|null $nodeParent
*
* @return void
*/
private function updateStateOverrides(Node $node, ?BpNode $nodeParent): void
{
$parents = [];
if ($nodeParent !== null) {
$parents = [$nodeParent];
} else {
$parents = $node->getParents();
}
foreach ($parents as $parent) {
$parentStateOverrides = $parent->getStateOverrides();
unset($parentStateOverrides[$node->getName()]);
$parent->setStateOverrides($parentStateOverrides);
}
}
}

View file

@ -100,7 +100,6 @@ class ProcessChanges
/**
* @param $nodeName
* @param Node|null $parent
* @return $this
*/
public function copyNode($nodeName)
@ -211,7 +210,7 @@ class ProcessChanges
/**
* Number of stacked changes
*
* @return bool
* @return int
*/
public function count()
{

View file

@ -11,9 +11,9 @@ abstract class MonitoredNode extends Node
public function getLink()
{
if ($this->isMissing()) {
return Html::tag('a', ['href' => '#'], $this->getAlias());
return Html::tag('a', ['href' => '#'], $this->getAlias() ?? $this->getName());
} else {
return Html::tag('a', ['href' => $this->getUrl()], $this->getAlias());
return Html::tag('a', ['href' => $this->getUrl()], $this->getAlias() ?? $this->getName());
}
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicecommenthistoryQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicecommentQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicedowntimeQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicedowntimestarthistoryQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServiceflappingstarthistoryQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicegroupQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicenotificationQuery;
use Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatehistoryQuery;
use Zend_Db_Select;
trait CustomVarJoinTemplateOverride
{
private $customVarsJoinTemplate = '%1$s = %2$s.object_id AND %2$s.varname LIKE %3$s';
/**
* This is a 1:1 copy of {@see IdoQuery::joinCustomvar()} to be able to
* adjust {@see IdoQuery::$customVarsJoinTemplate} as it's private
*/
protected function joinCustomvar($customvar)
{
// TODO: This is not generic enough yet
list($type, $name) = $this->customvarNameToTypeName($customvar);
$alias = ($type === 'host' ? 'hcv_' : 'scv_') . preg_replace('~[^a-zA-Z0-9_]~', '_', $name);
// We're replacing any problematic char with an underscore, which will lead to duplicates, this avoids them
$from = $this->select->getPart(Zend_Db_Select::FROM);
for ($i = 2; array_key_exists($alias, $from); $i++) {
$alias = $alias . '_' . $i;
}
$this->customVars[strtolower($customvar)] = $alias;
if ($type === 'host') {
if ($this instanceof ServicecommentQuery
|| $this instanceof ServicedowntimeQuery
|| $this instanceof ServicecommenthistoryQuery
|| $this instanceof ServicedowntimestarthistoryQuery
|| $this instanceof ServiceflappingstarthistoryQuery
|| $this instanceof ServicegroupQuery
|| $this instanceof ServicenotificationQuery
|| $this instanceof ServicestatehistoryQuery
|| $this instanceof \Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatusQuery
) {
$this->requireVirtualTable('services');
$leftcol = 's.host_object_id';
} else {
$leftcol = 'ho.object_id';
if (! $this->hasJoinedTable('ho')) {
$this->requireVirtualTable('hosts');
}
}
} else { // $type === 'service'
$leftcol = 'so.object_id';
if (! $this->hasJoinedTable('so')) {
$this->requireVirtualTable('services');
}
}
$mapped = $this->getMappedField($leftcol);
if ($mapped !== null) {
$this->requireColumn($leftcol);
$leftcol = $mapped;
}
$joinOn = sprintf(
$this->customVarsJoinTemplate,
$leftcol,
$alias,
$this->db->quote($name)
);
$this->select->joinLeft(
array($alias => $this->prefix . 'customvariablestatus'),
$joinOn,
array()
);
return $this;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
class HostStatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
{
use CustomVarJoinTemplateOverride;
}

View file

@ -0,0 +1,8 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
class ServiceStatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatusQuery
{
use CustomVarJoinTemplateOverride;
}

View file

@ -0,0 +1,16 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\DataView;
use Icinga\Data\ConnectionInterface;
use Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query\HostStatusQuery;
class HostStatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
{
public function __construct(ConnectionInterface $connection, array $columns = null)
{
parent::__construct($connection, $columns);
$this->query = new HostStatusQuery($connection->getResource(), $columns);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Icinga\Module\Businessprocess\Monitoring\DataView;
use Icinga\Data\ConnectionInterface;
use Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query\ServiceStatusQuery;
class ServiceStatus extends \Icinga\Module\Monitoring\DataView\Servicestatus
{
public function __construct(ConnectionInterface $connection, array $columns = null)
{
parent::__construct($connection, $columns);
$this->query = new ServiceStatusQuery($connection->getResource(), $columns);
}
}

View file

@ -4,6 +4,7 @@ namespace Icinga\Module\Businessprocess;
use Icinga\Exception\ProgrammingError;
use ipl\Html\Html;
use ipl\Web\Widget\Icon;
abstract class Node
{
@ -46,7 +47,7 @@ abstract class Node
self::NODE_EMPTY => 0
);
/** @var string Alias of the node */
/** @var ?string Alias of the node */
protected $alias;
/**
@ -73,7 +74,7 @@ abstract class Node
/**
* Node state
*
* @var int
* @var ?int
*/
protected $state;
@ -97,7 +98,7 @@ abstract class Node
/**
* This node's icon
*
* @var string
* @var ?string
*/
protected $icon;
@ -345,7 +346,7 @@ abstract class Node
/**
* Get the alias of the node
*
* @return string
* @return ?string
*/
public function getAlias()
{
@ -442,7 +443,7 @@ abstract class Node
throw new ProgrammingError(
'Got invalid state for node %s: %s',
$this->getName(),
var_export($state, 1) . var_export($this->stateToSortStateMap, 1)
var_export($state, true) . var_export($this->stateToSortStateMap, true)
);
}
@ -463,14 +464,12 @@ abstract class Node
public function getLink()
{
return Html::tag('a', ['href' => '#', 'class' => 'toggle'], Html::tag('i', [
'class' => 'icon icon-down-dir'
]));
return Html::tag('a', ['href' => '#', 'class' => 'toggle'], new Icon('caret-down'));
}
public function getIcon()
public function getIcon(): Icon
{
return Html::tag('i', ['class' => 'icon icon-' . ($this->icon ?: 'attention-circled')]);
return new Icon($this->icon ?? 'circle-exclamation');
}
public function operatorHtml()
@ -483,6 +482,31 @@ abstract class Node
return $this->name;
}
/**
* Get the Node operators
*
* @return array
*/
public static function getOperators(): array
{
return [
'&' => t('AND'),
'|' => t('OR'),
'^' => t('XOR'),
'!' => t('NOT'),
'%' => t('DEGRADED'),
'1' => t('MIN 1'),
'2' => t('MIN 2'),
'3' => t('MIN 3'),
'4' => t('MIN 4'),
'5' => t('MIN 5'),
'6' => t('MIN 6'),
'7' => t('MIN 7'),
'8' => t('MIN 8'),
'9' => t('MIN 9'),
];
}
public function getIdentifier()
{
return '@' . $this->getBpConfig()->getName() . ':' . $this->getName();

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Icingadb\Hook\HostActionsHook;
use Icinga\Module\Icingadb\Model\Host;
use ipl\Web\Widget\Link;
@ -15,7 +16,7 @@ class HostActions extends HostActionsHook
new Link(
$label,
'businessprocess/node/impact?name='
. rawurlencode($host->name . ';Hoststatus')
. rawurlencode(BpConfig::joinNodeName($host->name, 'Hoststatus'))
)
);
}

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Icingadb\Hook\ServiceActionsHook;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Web\Widget\Link;
@ -16,9 +17,7 @@ class ServiceActions extends ServiceActionsHook
$label,
sprintf(
'businessprocess/node/impact?name=%s',
rawurlencode(
sprintf('%s;%s', $service->host->name, $service->name)
)
rawurlencode(BpConfig::joinNodeName($service->host->name, $service->name))
)
)
);

View file

@ -16,7 +16,7 @@ use ipl\Html\ValidHtml;
class ServiceDetailExtension extends ServiceDetailExtensionHook
{
/** @var LegacyStorage */
/** @var ?LegacyStorage */
private $storage;
/** @var string */

View file

@ -13,7 +13,7 @@ use Icinga\Module\Monitoring\Object\Service;
class DetailviewExtension extends DetailviewExtensionHook
{
/** @var LegacyStorage */
/** @var ?LegacyStorage */
private $storage;
/** @var string */

View file

@ -2,6 +2,7 @@
namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Monitoring\Hook\HostActionsHook;
use Icinga\Module\Monitoring\Object\Host;
@ -12,7 +13,7 @@ class HostActions extends HostActionsHook
$label = mt('businessprocess', 'Business Impact');
return array(
$label => 'businessprocess/node/impact?name='
. rawurlencode($host->getName() . ';Hoststatus')
. rawurlencode(BpConfig::joinNodeName($host->getName(), 'Hoststatus'))
);
}
}

View file

@ -4,6 +4,7 @@ namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
use Exception;
use Icinga\Application\Config;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
use Icinga\Module\Monitoring\Object\Service;
use Icinga\Web\Url;
@ -16,9 +17,7 @@ class ServiceActions extends ServiceActionsHook
return array(
$label => sprintf(
'businessprocess/node/impact?name=%s',
rawurlencode(
sprintf('%s;%s', $service->getHost()->getName(), $service->getName())
)
rawurlencode(BpConfig::joinNodeName($service->getHost()->getName(), $service->getName()))
)
);
}

View file

@ -7,6 +7,7 @@ use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
use Icinga\Module\Businessprocess\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Web\Widget\Icon;
class Breadcrumb extends BaseHtmlElement
{
@ -37,7 +38,7 @@ class Breadcrumb extends BaseHtmlElement
'href' => Url::fromPath('businessprocess'),
'title' => mt('businessprocess', 'Show Overview')
],
Html::tag('i', ['class' => 'icon icon-home'])
new Icon('house')
)
));
$breadcrumb->add(Html::tag('li')->add(
@ -47,6 +48,7 @@ class Breadcrumb extends BaseHtmlElement
$parts = array();
while ($nodeName = array_pop($path)) {
/** @var BpNode $node */
$node = $bp->getNode($nodeName);
$renderer->setParentNode($node);
array_unshift(

View file

@ -5,6 +5,7 @@ namespace Icinga\Module\Businessprocess\Renderer;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Common\Sort;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\MonitoredNode;
use Icinga\Module\Businessprocess\Node;
@ -12,10 +13,13 @@ use Icinga\Module\Businessprocess\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlDocument;
use ipl\Stdlib\Str;
use ipl\Web\Widget\StateBadge;
abstract class Renderer extends HtmlDocument
{
use Sort;
/** @var BpConfig */
protected $config;
@ -120,6 +124,37 @@ abstract class Renderer extends HtmlDocument
}
}
/**
* Get the default sort specification
*
* @return string
*/
public function getDefaultSort(): string
{
if ($this->config->getMetadata()->isManuallyOrdered()) {
return 'manual asc';
}
return 'display_name asc';
}
/**
* Get whether a custom sort order is applied
*
* @return bool
*/
public function appliesCustomSorting(): bool
{
if (empty($this->getSort())) {
return false;
}
list($sortBy, $_) = Str::symmetricSplit($this->getSort(), ' ', 2);
list($defaultSortBy, $_) = Str::symmetricSplit($this->getDefaultSort(), ' ', 2);
return $sortBy !== $defaultSortBy;
}
/**
* @return int
*/
@ -134,12 +169,10 @@ abstract class Renderer extends HtmlDocument
/**
* @param $summary
* @return BaseHtmlElement
* @return ?BaseHtmlElement
*/
public function renderStateBadges($summary, $totalChildren)
{
$elements = [];
$itemCount = Html::tag(
'span',
[
@ -150,7 +183,7 @@ abstract class Renderer extends HtmlDocument
sprintf(mtp('businessprocess', '%u Child', '%u Children', $totalChildren), $totalChildren)
);
$elements[] = array_filter([
$elements = array_filter([
$this->createBadgeGroup($summary, 'CRITICAL'),
$this->createBadgeGroup($summary, 'UNKNOWN'),
$this->createBadgeGroup($summary, 'WARNING'),
@ -265,7 +298,7 @@ abstract class Renderer extends HtmlDocument
*/
public function getId(Node $node, $path)
{
return md5((empty($path) ? '' : implode(';', $path)) . $node->getName());
return 'businessprocess-' . md5((empty($path) ? '' : implode(';', $path)) . $node->getName());
}
public function setPath(array $path)

View file

@ -17,7 +17,9 @@ class TileRenderer extends Renderer
[
'class' => ['sortable', 'tiles', $this->howMany()],
'data-base-target' => '_self',
'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
'data-sortable-disabled' => $this->isLocked() || $this->appliesCustomSorting()
? 'true'
: 'false',
'data-sortable-data-id-attr' => 'id',
'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists
'data-csrf-token' => CsrfToken::generate()
@ -43,10 +45,8 @@ class TileRenderer extends Renderer
->getAbsoluteUrl());
}
$nodes = $this->getChildNodes();
$path = $this->getCurrentPath();
foreach ($nodes as $name => $node) {
foreach ($this->sort($this->getChildNodes()) as $name => $node) {
$this->add(new NodeTile($this, $node, $path));
}

View file

@ -4,15 +4,15 @@ namespace Icinga\Module\Businessprocess\Renderer\TileRenderer;
use Icinga\Date\DateFormatter;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\HostNode;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\MonitoredNode;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Renderer\Renderer;
use Icinga\Module\Businessprocess\ServiceNode;
use Icinga\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
use ipl\Web\Widget\StateBall;
class NodeTile extends BaseHtmlElement
@ -35,9 +35,8 @@ class NodeTile extends BaseHtmlElement
/**
* NodeTile constructor.
* @param Renderer $renderer
* @param $name
* @param Node $node
* @param null $path
* @param ?array $path
*/
public function __construct(Renderer $renderer, Node $node, $path = null)
{
@ -103,14 +102,7 @@ class NodeTile extends BaseHtmlElement
$this->add($link);
} else {
$this->add(Html::tag(
'a',
Html::tag(
'span',
['style' => 'font-size: 75%'],
sprintf('Trying to access a missing business process node "%s"', $node->getNodeName())
)
));
$this->add(new Link($node->getAlias(), $this->getMainNodeUrl($node)->getAbsoluteUrl()));
}
if ($this->renderer->rendersSubNode()
@ -180,7 +172,11 @@ class NodeTile extends BaseHtmlElement
$node = $this->node;
$url = $this->getMainNodeUrl($node);
if ($node instanceof MonitoredNode) {
$link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getAlias());
$link = Html::tag(
'a',
['href' => $url, 'data-base-target' => '_next'],
$node->getAlias() ?? $node->getName()
);
} else {
$link = Html::tag('a', ['href' => $url], $node->getAlias());
}
@ -200,28 +196,31 @@ class NodeTile extends BaseHtmlElement
'href' => $url->with('mode', 'tile'),
'title' => mt('businessprocess', 'Show tiles for this subtree')
],
Html::tag('i', ['class' => 'icon icon-dashboard'])
new Icon('grip')
))->add(Html::tag(
'a',
[
'href' => $url->with('mode', 'tree'),
'title' => mt('businessprocess', 'Show this subtree as a tree')
],
Html::tag('i', ['class' => 'icon icon-sitemap'])
new Icon('sitemap')
));
if ($node instanceof ImportedNode) {
if ($node->getBpConfig()->hasNode($node->getName())) {
$bpConfig = $node->getBpConfig();
if ($bpConfig->isFaulty() || $bpConfig->hasNode($node->getName())) {
$this->actions()->add(Html::tag(
'a',
[
'data-base-target' => '_next',
'href' => $this->renderer->getSourceUrl($node)->getAbsoluteUrl(),
'href' => $bpConfig->isFaulty()
? $this->renderer->getBaseUrl()->setParam('config', $bpConfig->getName())
: $this->renderer->getSourceUrl($node)->getAbsoluteUrl(),
'title' => mt(
'businessprocess',
'Show this process as part of its original configuration'
)
],
Html::tag('i', ['class' => 'icon icon-forward'])
new Icon('share')
));
}
}
@ -236,7 +235,7 @@ class NodeTile extends BaseHtmlElement
'class' => 'node-info',
'title' => sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
],
Html::tag('i', ['class' => 'icon icon-info-circled'])
new Icon('info')
);
if (preg_match('#^http(?:s)?://#', $url)) {
$link->addAttributes(['target' => '_blank']);
@ -244,20 +243,17 @@ class NodeTile extends BaseHtmlElement
$this->actions()->add($link);
}
} else {
// $url = $this->makeMonitoredNodeUrl($node);
if ($node instanceof ServiceNode) {
$this->actions()->add(Html::tag(
'a',
['href' => $node->getUrl(), 'data-base-target' => '_next'],
Html::tag('i', ['class' => 'icon icon-service'])
));
} elseif ($node instanceof HostNode) {
$this->actions()->add(Html::tag(
'a',
['href' => $node->getUrl(), 'data-base-target' => '_next'],
Html::tag('i', ['class' => 'icon icon-host'])
));
}
$this->actions()->add(Html::tag(
'a',
['href' => $node->getUrl(), 'data-base-target' => '_next'],
$node->getIcon()
));
}
if ($node->isAcknowledged()) {
$this->actions()->add(new Icon('check', ['class' => 'handled-icon']));
} elseif ($node->isInDowntime()) {
$this->actions()->add(new Icon('plug', ['class' => 'handled-icon']));
}
}
@ -291,7 +287,7 @@ class NodeTile extends BaseHtmlElement
'Show the business impact of this node by simulating a specific state'
)
],
Html::tag('i', ['class' => 'icon icon-magic'])
new Icon('wand-magic-sparkles')
));
$this->actions()->add(Html::tag(
@ -302,7 +298,7 @@ class NodeTile extends BaseHtmlElement
->with('editmonitorednode', $this->node->getName()),
'title' => mt('businessprocess', 'Modify this monitored node')
],
Html::tag('i', ['class' => 'icon icon-edit'])
new Icon('edit')
));
}
@ -319,7 +315,7 @@ class NodeTile extends BaseHtmlElement
->with('editnode', $this->node->getName()),
'title' => mt('businessprocess', 'Modify this business process node')
],
Html::tag('i', ['class' => 'icon icon-edit'])
new Icon('edit')
));
$addUrl = $baseUrl->with([
@ -333,7 +329,7 @@ class NodeTile extends BaseHtmlElement
'href' => $addUrl,
'title' => mt('businessprocess', 'Add a new sub-node to this business process')
],
Html::tag('i', ['class' => 'icon icon-plus'])
new Icon('plus')
));
}
}
@ -350,7 +346,7 @@ class NodeTile extends BaseHtmlElement
'href' => $baseUrl->with($params),
'title' => mt('businessprocess', 'Delete this node')
],
Html::tag('i', ['class' => 'icon icon-cancel'])
new Icon('xmark')
));
}
}

View file

@ -2,19 +2,24 @@
namespace Icinga\Module\Businessprocess\Renderer;
use Icinga\Application\Version;
use Icinga\Date\DateFormatter;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\ImportedNode;
use Icinga\Module\Businessprocess\Node;
use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
use Icinga\Module\Icingadb\Model\State;
use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\HtmlElement;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\StateBall;
class TreeRenderer extends Renderer
{
const NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE = '2.11.2';
public function assemble()
{
$bp = $this->config;
@ -24,7 +29,9 @@ class TreeRenderer extends Renderer
[
'id' => $htmlId,
'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'],
'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false',
'data-sortable-disabled' => $this->isLocked() || $this->appliesCustomSorting()
? 'true'
: 'false',
'data-sortable-data-id-attr' => 'id',
'data-sortable-direction' => 'vertical',
'data-sortable-group' => json_encode([
@ -32,7 +39,6 @@ class TreeRenderer extends Renderer
'put' => 'function:rowPutAllowed'
]),
'data-sortable-invert-swap' => 'true',
'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false',
'data-csrf-token' => CsrfToken::generate()
],
$this->renderBp($bp)
@ -42,6 +48,10 @@ class TreeRenderer extends Renderer
'data-action-url',
$this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
);
if (version_compare(Version::VERSION, self::NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE, '<')) {
$tree->getAttributes()->add('data-is-root-config', true);
}
} else {
$nodeName = $this->parent instanceof ImportedNode
? $this->parent->getNodeName()
@ -61,18 +71,18 @@ class TreeRenderer extends Renderer
/**
* @param BpConfig $bp
* @return string
* @return array
*/
public function renderBp(BpConfig $bp)
{
$html = array();
$html = [];
if ($this->wantsRootNodes()) {
$nodes = $bp->getChildren();
$nodes = $bp->getRootNodes();
} else {
$nodes = $this->parent->getChildren();
}
foreach ($nodes as $name => $node) {
foreach ($this->sort($nodes) as $name => $node) {
if ($node instanceof BpNode) {
$html[] = $this->renderNode($bp, $node);
} else {
@ -110,7 +120,7 @@ class TreeRenderer extends Renderer
{
$icons = [];
if (empty($path) && $node instanceof BpNode) {
$icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']);
$icons[] = new Icon('sitemap');
} else {
$icons[] = $node->getIcon();
}
@ -125,12 +135,13 @@ class TreeRenderer extends Renderer
DateFormatter::timeSince($node->getLastStateChange())
)
]);
if ($node->isInDowntime()) {
$icons[] = Html::tag('i', ['class' => 'icon icon-plug']);
}
if ($node->isAcknowledged()) {
$icons[] = Html::tag('i', ['class' => 'icon icon-ok']);
$icons[] = new Icon('check');
} elseif ($node->isInDowntime()) {
$icons[] = new Icon('plug');
}
return $icons;
}
@ -146,7 +157,8 @@ class TreeRenderer extends Renderer
)
])
);
$overriddenState->add(Html::tag('i', ['class' => 'icon icon-right-small']));
$overriddenState->add(new Icon('arrow-right'));
$overriddenState->add(
(new StateBall(strtolower($node->getStateName($fakeState)), StateBall::SIZE_MEDIUM))
->addAttributes([
@ -192,36 +204,48 @@ class TreeRenderer extends Renderer
$attributes->add('class', 'node');
}
$div = Html::tag('div');
$li->add($div);
$details = new HtmlElement('details', Attributes::create(['open' => true]));
$summary = new HtmlElement('summary');
if (version_compare(Version::VERSION, self::NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE, '>=')) {
$details->getAttributes()->add('class', 'collapsible');
$summary->getAttributes()->add('class', 'collapsible-control'); // Helps JS, improves performance a bit
}
$div->add($node->getLink());
$div->add($this->getNodeIcons($node, $path));
$summary->addHtml(
new Icon('caret-down', ['class' => 'collapse-icon']),
new Icon('caret-right', ['class' => 'expand-icon'])
);
$div->add(Html::tag('span', null, $node->getAlias()));
$summary->add($this->getNodeIcons($node, $path));
$summary->add(Html::tag('span', null, $node->getAlias()));
if ($node instanceof BpNode) {
$div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml()));
$summary->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml()));
}
if ($node instanceof BpNode && $node->hasInfoUrl()) {
$div->add($this->createInfoAction($node));
$summary->add($this->createInfoAction($node));
}
$differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName();
if (! $this->isLocked() && !$differentConfig) {
$div->add($this->getActionIcons($bp, $node));
$summary->add($this->getActionIcons($bp, $node));
} elseif ($differentConfig) {
$div->add($this->actionIcon(
'forward',
$this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(),
$summary->add($this->actionIcon(
'share',
$node->getBpConfig()->isFaulty()
? $this->getBaseUrl()->setParam('config', $node->getBpConfig()->getName())
: $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(),
mt('businessprocess', 'Show this process as part of its original configuration')
)->addAttributes(['data-base-target' => '_next']));
}
$ul = Html::tag('ul', [
'class' => ['bp', 'sortable'],
'data-sortable-disabled' => ($this->isLocked() || $differentConfig) ? 'true' : 'false',
'data-sortable-disabled' => ($this->isLocked() || $differentConfig || $this->appliesCustomSorting())
? 'true'
: 'false',
'data-sortable-invert-swap' => 'true',
'data-sortable-data-id-attr' => 'id',
'data-sortable-draggable' => '.movable',
@ -240,10 +264,9 @@ class TreeRenderer extends Renderer
])
->getAbsoluteUrl()
]);
$li->add($ul);
$path[] = $differentConfig ? $node->getIdentifier() : $node->getName();
foreach ($node->getChildren() as $name => $child) {
foreach ($this->sort($node->getChildren()) as $name => $child) {
if ($child instanceof BpNode) {
$ul->add($this->renderNode($bp, $child, $path));
} else {
@ -251,6 +274,10 @@ class TreeRenderer extends Renderer
}
}
$details->addHtml($summary);
$details->addHtml($ul);
$li->addHtml($details);
return $li;
}
@ -307,7 +334,7 @@ class TreeRenderer extends Renderer
protected function createSimulationAction(BpConfig $bp, Node $node)
{
return $this->actionIcon(
'magic',
'wand-magic-sparkles',
$this->getUrl()->with(array(
//'config' => $bp->getName(),
'action' => 'simulation',
@ -321,7 +348,7 @@ class TreeRenderer extends Renderer
{
$url = $node->getInfoUrl();
return $this->actionIcon(
'help',
'question',
$url,
sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
)->addAttributes(['target' => '_blank']);
@ -336,7 +363,7 @@ class TreeRenderer extends Renderer
'title' => $title,
'class' => 'action-link'
],
Html::tag('i', ['class' => 'icon icon-' . $icon])
new Icon($icon)
);
}

View file

@ -3,9 +3,12 @@
namespace Icinga\Module\Businessprocess;
use Icinga\Module\Businessprocess\Web\Url;
use ipl\I18n\Translation;
class ServiceNode extends MonitoredNode
{
use Translation;
protected $hostname;
/** @var string Alias of the host */
@ -15,11 +18,11 @@ class ServiceNode extends MonitoredNode
protected $className = 'service';
protected $icon = 'service';
protected $icon = 'gear';
public function __construct($object)
{
$this->name = $object->hostname . ';' . $object->service;
$this->name = BpConfig::joinNodeName($object->hostname, $object->service);
$this->hostname = $object->hostname;
$this->service = $object->service;
if (isset($object->state)) {
@ -65,7 +68,15 @@ class ServiceNode extends MonitoredNode
public function getAlias()
{
return $this->getHostAlias() . ': ' . $this->alias;
if ($this->getHostAlias() === null || $this->alias === null) {
return null;
}
return sprintf(
$this->translate('%s on %s', '<service> on <host>'),
$this->alias,
$this->getHostAlias()
);
}
public function getUrl()

View file

@ -7,6 +7,7 @@ use Icinga\Application\Benchmark;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\IcingaDbObject;
use Icinga\Module\Businessprocess\ServiceNode;
use Icinga\Module\Icingadb\Common\IcingaRedis;
use Icinga\Module\Icingadb\Model\Host;
use Icinga\Module\Icingadb\Model\Service;
use ipl\Sql\Connection as IcingaDbConnection;
@ -52,43 +53,97 @@ class IcingaDbState
{
$config = $this->config;
$involvedHostNames = $config->listInvolvedHostNames();
if (empty($involvedHostNames)) {
return $this;
}
Benchmark::measure(sprintf(
'Retrieving states for business process %s using Icinga DB backend',
$config->getName()
));
$hosts = $config->listInvolvedHostNames();
if (empty($hosts)) {
return $this;
}
$hosts = Host::on($this->backend)->columns([
'id' => 'host.id',
'name' => 'host.name',
'display_name' => 'host.display_name',
'hard_state' => 'host.state.hard_state',
'soft_state' => 'host.state.soft_state',
'last_state_change' => 'host.state.last_state_change',
'in_downtime' => 'host.state.in_downtime',
'is_acknowledged' => 'host.state.is_acknowledged'
])->filter(Filter::equal('host.name', $involvedHostNames));
$queryHost = Host::on($this->backend)->with('state');
$queryHost->filter(Filter::equal('host.name', $hosts));
$services = Service::on($this->backend)->columns([
'id' => 'service.id',
'name' => 'service.name',
'display_name' => 'service.display_name',
'host_name' => 'host.name',
'host_display_name' => 'host.display_name',
'hard_state' => 'service.state.hard_state',
'soft_state' => 'service.state.soft_state',
'last_state_change' => 'service.state.last_state_change',
'in_downtime' => 'service.state.in_downtime',
'is_acknowledged' => 'service.state.is_acknowledged'
])->filter(Filter::equal('host.name', $involvedHostNames));
$hostObject = $queryHost->getModel()->getTableName();
Benchmark::measure('Retrieved states for ' . $queryHost->count() . ' hosts in ' . $config->getName());
$queryService = Service::on($this->backend)
->with('state')
->with('host')
->with('host.state');
$queryService->filter(Filter::equal('host.name', $hosts));
Benchmark::measure('Retrieved states for ' . $queryService->count() . ' services in ' . $config->getName());
$configs = $config->listInvolvedConfigs();
$serviceObject = $queryService->getModel()->getTableName();
foreach ($configs as $cfg) {
foreach ($queryService as $row) {
$this->handleDbRow($row, $cfg, $serviceObject);
// All of this is ipl-sql now, for performance reasons
foreach ($config->listInvolvedConfigs() as $cfg) {
$serviceIds = [];
$serviceResults = [];
foreach ($this->backend->yieldAll($services->assembleSelect()) as $row) {
$row->hex_id = bin2hex(is_resource($row->id) ? stream_get_contents($row->id) : $row->id);
$serviceIds[] = $row->hex_id;
$serviceResults[] = $row;
}
foreach ($queryHost as $row) {
$this->handleDbRow($row, $cfg, $hostObject);
$redisServiceResults = iterator_to_array(IcingaRedis::fetchServiceState($serviceIds, [
'hard_state',
'soft_state',
'last_state_change',
'in_downtime',
'is_acknowledged'
]));
foreach ($serviceResults as $row) {
if (isset($redisServiceResults[$row->hex_id])) {
$row = (object) array_merge(
(array) $row,
$redisServiceResults[$row->hex_id]
);
}
$this->handleDbRow($row, $cfg, 'service');
}
Benchmark::measure('Retrieved states for ' . count($serviceIds) . ' services in ' . $config->getName());
$hostIds = [];
$hostResults = [];
foreach ($this->backend->yieldAll($hosts->assembleSelect()) as $row) {
$row->hex_id = bin2hex(is_resource($row->id) ? stream_get_contents($row->id) : $row->id);
$hostIds[] = $row->hex_id;
$hostResults[] = $row;
}
$redisHostResults = iterator_to_array(IcingaRedis::fetchHostState($hostIds, [
'hard_state',
'soft_state',
'last_state_change',
'in_downtime',
'is_acknowledged'
]));
foreach ($hostResults as $row) {
if (isset($redisHostResults[$row->hex_id])) {
$row = (object) array_merge(
(array) $row,
$redisHostResults[$row->hex_id]
);
}
$this->handleDbRow($row, $cfg, 'host');
}
Benchmark::measure('Retrieved states for ' . count($hostIds) . ' hosts in ' . $config->getName());
}
Benchmark::measure('Got states for business process ' . $config->getName());
@ -96,12 +151,12 @@ class IcingaDbState
return $this;
}
protected function handleDbRow($row, BpConfig $config, $objectName)
protected function handleDbRow($row, BpConfig $config, $type)
{
if ($objectName === 'service') {
$key = $row->host->name . ';' . $row->name;
if ($type === 'service') {
$key = BpConfig::joinNodeName($row->host_name, $row->name);
} else {
$key = $row->name . ';Hoststatus';
$key = BpConfig::joinNodeName($row->name, 'Hoststatus');
}
// We fetch more states than we need, so skip unknown ones
@ -112,29 +167,25 @@ class IcingaDbState
$node = $config->getNode($key);
if ($this->config->usesHardStates()) {
if ($row->state->hard_state !== null) {
$node->setState($row->state->hard_state)->setMissing(false);
if ($row->hard_state !== null) {
$node->setState($row->hard_state)->setMissing(false);
}
} else {
if ($row->state->soft_state !== null) {
$node->setState($row->state->soft_state)->setMissing(false);
if ($row->soft_state !== null) {
$node->setState($row->soft_state)->setMissing(false);
}
}
if ($row->state->last_state_change !== null) {
$node->setLastStateChange($row->state->last_state_change/1000);
}
if ($row->state->in_downtime) {
$node->setDowntime(true);
}
if ($row->state->is_acknowledged) {
$node->setAck(true);
if ($row->last_state_change !== null) {
$node->setLastStateChange($row->last_state_change / 1000.0);
}
$node->setDowntime($row->in_downtime === 'y');
$node->setAck($row->is_acknowledged === 'y');
$node->setAlias($row->display_name);
if ($node instanceof ServiceNode) {
$node->setHostAlias($row->host->display_name);
$node->setHostAlias($row->host_display_name);
}
}
}

View file

@ -115,12 +115,12 @@ class MonitoringState
protected function handleDbRow($row, BpConfig $config)
{
$key = $row->hostname;
if (property_exists($row, 'service')) {
$key .= ';' . $row->service;
} else {
$key .= ';Hoststatus';
}
$key = BpConfig::joinNodeName(
$row->hostname,
property_exists($row, 'service')
? $row->service
: 'Hoststatus'
);
// We fetch more states than we need, so skip unknown ones
if (! $config->hasNode($key)) {

View file

@ -11,7 +11,7 @@ use Icinga\Module\Businessprocess\Metadata;
class LegacyConfigParser
{
/** @var string */
/** @var ?string */
protected static $prevKey;
/** @var int */
@ -196,6 +196,10 @@ class LegacyConfigParser
*/
protected static function parseHeaderLine($line, Metadata $metadata)
{
if (empty($line)) {
return;
}
if (preg_match('/^\s*#\s+(.+?)\s*:\s*(.+)$/', trim($line), $m)) {
if ($metadata->hasKey($m[1])) {
static::$prevKey = $m[1];
@ -226,33 +230,16 @@ class LegacyConfigParser
*/
protected function parseDisplay(&$line, BpConfig $bp)
{
list($display, $name, $desc) = preg_split('~\s*;\s*~', substr($line, 8), 3);
list($display, $name, $desc) = preg_split('~\s*(?<!\\\\);\s*~', substr($line, 8), 3);
$bp->getBpNode($name)->setAlias($desc)->setDisplay($display);
if ($display > 0) {
$bp->addRootNode($name);
}
}
/**
* @param $line
* @param BpConfig $bp
*/
protected function parseExternalInfo(&$line, BpConfig $bp)
{
list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2);
$bp->getBpNode($name)->setInfoCommand($script);
}
protected function parseExtraInfo(&$line, BpConfig $bp)
{
// TODO: Not yet
// list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2);
// $this->getNode($name)->setExtraInfo($script);
}
protected function parseInfoUrl(&$line, BpConfig $bp)
{
list($name, $url) = preg_split('~\s*;\s*~', substr($line, 9), 2);
list($name, $url) = preg_split('~\s*(?<!\\\\);\s*~', substr($line, 9), 2);
$bp->getBpNode($name)->setInfoUrl($url);
}
@ -260,6 +247,7 @@ class LegacyConfigParser
{
// state_overrides <bp-node>!<child>|n-n[,n-n]!<child>|n-n[,n-n]
$segments = preg_split('~\s*!\s*~', substr($line, 16));
/** @var BpNode $node */
$node = $bp->getNode(array_shift($segments));
foreach ($segments as $overrideDef) {
list($childName, $overrides) = preg_split('~\s*\|\s*~', $overrideDef, 2);
@ -284,10 +272,7 @@ class LegacyConfigParser
switch ($type) {
case 'external_info':
$this->parseExternalInfo($line, $bp);
break;
case 'extra_info':
$this->parseExtraInfo($line, $bp);
break;
case 'info_url':
$this->parseInfoUrl($line, $bp);
@ -340,12 +325,8 @@ class LegacyConfigParser
list($name, $value) = preg_split('~\s*=\s*~', $line, 2);
if (strpos($name, ';') !== false) {
$this->parseError('No semicolon allowed in varname');
}
$op = '&';
if (preg_match_all('~(?<!\\\\)([\|\+&\!\%])~', $value, $m)) {
if (preg_match_all('~(?<!\\\\)([\|\+&\!\%\^])~', $value, $m)) {
$op = implode('', $m[1]);
for ($i = 1; $i < strlen($op); $i++) {
if ($op[$i] !== $op[$i - 1]) {
@ -374,16 +355,16 @@ class LegacyConfigParser
$cmps = preg_split('~\s*(?<!\\\\)\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY);
foreach ($cmps as $val) {
$val = preg_replace('~(\\\\([\|\+&\!\%]))~', '$2', $val);
if (strpos($val, ';') !== false) {
$val = preg_replace('~(\\\\([\|\+&\!\%\^]))~', '$2', $val);
if (preg_match('~(?<!\\\\);~', $val)) {
if ($bp->hasNode($val)) {
$node->addChild($bp->getNode($val));
} else {
list($host, $service) = preg_split('~;~', $val, 2);
list($host, $service) = preg_split('~(?<!\\\\);~', $val, 2);
if ($service === 'Hoststatus') {
$node->addChild($bp->createHost($host));
$node->addChild($bp->createHost(str_replace('\\;', ';', $host)));
} else {
$node->addChild($bp->createService($host, $service));
$node->addChild($bp->createService(str_replace('\\;', ';', $host), $service));
}
}
} elseif ($val[0] === '@') {

View file

@ -11,6 +11,8 @@ class LegacyConfigRenderer
/** @var array */
protected $renderedNodes;
protected $config;
/**
* LecagyConfigRenderer constructor
*
@ -197,7 +199,7 @@ class LegacyConfigRenderer
$op = static::renderOperator($node);
$children = $node->getChildNames();
$str = implode(' ' . $op . ' ', array_map(function ($val) {
return preg_replace('~([\|\+&\!\%])~', '\\\\$1', $val);
return preg_replace('~([\|\+&\!\%\^])~', '\\\\$1', $val);
}, $children));
if ((count($children) < 2) && $op !== '&') {

View file

@ -73,7 +73,6 @@ class LegacyStorage extends Storage
$files[$name] = $meta->getExtendedTitle();
}
natcasesort($files);
return $files;
}
@ -93,7 +92,6 @@ class LegacyStorage extends Storage
$files[$name] = $name;
}
natcasesort($files);
return $files;
}

View file

@ -7,21 +7,21 @@ use Icinga\Application\ApplicationBootstrap;
use Icinga\Application\Icinga;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Storage\LegacyStorage;
use Icinga\Module\Businessprocess\Web\FakeRequest;
use PHPUnit_Framework_TestCase;
abstract class BaseTestCase extends PHPUnit_Framework_TestCase
abstract class BaseTestCase extends \Icinga\Test\BaseTestCase
{
/** @var ApplicationBootstrap */
private static $app;
/**
* @inheritdoc
*/
public function setUp()
public function setUp(): void
{
$this->app();
FakeRequest::setConfiguredBaseUrl('/icingaweb2/');
parent::setUp();
$this->getRequestMock()->shouldReceive('getBaseUrl')->andReturn('/icingaweb2/');
$this->app()
->getModuleManager()
->loadModule('businessprocess');
}
protected function emptyConfigSection()
@ -49,7 +49,7 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase
}
/**
* @param null $subDir
* @param ?string $subDir
* @return string
*/
protected function getTestsBaseDir($subDir = null)

View file

@ -3,10 +3,12 @@
namespace Icinga\Module\Businessprocess\Web\Component;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Html\Text;
use ipl\Web\Url;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
class BpDashboardTile extends BaseHtmlElement
{
@ -16,14 +18,10 @@ class BpDashboardTile extends BaseHtmlElement
public function __construct(BpConfig $bp, $title, $description, $icon, $url, $urlParams = null, $attributes = null)
{
if (! isset($attributes['href'])) {
$attributes['href'] = Url::fromPath($url, $urlParams ?: []);
}
$this->add(Html::tag(
'div',
['class' => 'bp-link', 'data-base-target' => '_main'],
Html::tag('a', $attributes, Html::tag('i', ['class' => 'icon icon-' . $icon]))
(new Link(new Icon($icon), Url::fromPath($url, $urlParams ?: []), $attributes))
->add(Html::tag('span', ['class' => 'header'], $title))
->add($description)
));

View file

@ -2,8 +2,10 @@
namespace Icinga\Module\Businessprocess\Web\Component;
use Exception;
use Icinga\Application\Modules\Module;
use Icinga\Authentication\Auth;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
use Icinga\Module\Businessprocess\State\IcingaDbState;
use Icinga\Module\Businessprocess\State\MonitoringState;
@ -87,13 +89,25 @@ class Dashboard extends BaseHtmlElement
foreach ($processes as $name) {
$meta = $storage->loadMetadata($name);
$title = $meta->get('Title');
if ($title) {
$title = sprintf('%s (%s)', $title, $name);
} else {
if ($title === null) {
$title = $name;
}
$bp = $storage->loadProcess($name);
try {
$bp = $storage->loadProcess($name);
} catch (Exception $e) {
$this->add(new BpDashboardTile(
new BpConfig(),
$title,
sprintf(t('File %s has faulty config'), $name . '.conf'),
'file-circle-xmark',
'businessprocess/process/show',
['config' => $name]
));
continue;
}
if (Module::exists('icingadb') &&
(! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())

View file

@ -5,6 +5,7 @@ namespace Icinga\Module\Businessprocess\Web\Component;
use Icinga\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\Web\Widget\Icon;
class DashboardAction extends BaseHtmlElement
{
@ -19,7 +20,7 @@ class DashboardAction extends BaseHtmlElement
}
$this->add(Html::tag('a', $attributes)
->add(Html::tag('i', ['class' => 'icon icon-' . $icon]))
->add(new Icon($icon))
->add(Html::tag('span', ['class' => 'header'], $title))
->add($description));
}

View file

@ -8,10 +8,11 @@ use Icinga\Module\Businessprocess\Renderer\Renderer;
use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
use Icinga\Web\Url;
use ipl\Html\Html;
use ipl\Web\Widget\Icon;
class RenderedProcessActionBar extends ActionBar
{
public function __construct(BpConfig $config, Renderer $renderer, Auth $auth, Url $url)
public function __construct(BpConfig $config, Renderer $renderer, Url $url)
{
$meta = $config->getMetadata();
@ -34,8 +35,8 @@ class RenderedProcessActionBar extends ActionBar
}
$link->add([
Html::tag('i', ['class' => 'icon icon-dashboard' . ($renderer instanceof TreeRenderer ? '' : ' active')]),
Html::tag('i', ['class' => 'icon icon-sitemap' . ($renderer instanceof TreeRenderer ? ' active' : '')])
new Icon('grip', ['class' => $renderer instanceof TreeRenderer ? null : 'active']),
new Icon('sitemap', ['class' => $renderer instanceof TreeRenderer ? 'active' : null])
]);
$this->add(
@ -50,9 +51,11 @@ class RenderedProcessActionBar extends ActionBar
'data-base-target' => '_main',
'href' => $url->with('showFullscreen', true),
'title' => mt('businessprocess', 'Switch to fullscreen mode'),
'class' => 'icon-resize-full-alt'
],
mt('businessprocess', 'Fullscreen')
[
new Icon('maximize'),
mt('businessprocess', 'Fullscreen')
]
));
$hasChanges = $config->hasSimulations() || $config->hasBeenChanged();
@ -66,8 +69,7 @@ class RenderedProcessActionBar extends ActionBar
'Imported processes can only be changed in their original configuration'
)
]);
$span->add(Html::tag('i', ['class' => 'icon icon-lock']))
->add(mt('businessprocess', 'Editing Locked'));
$span->add([new Icon('lock'), mt('businessprocess', 'Editing Locked')]);
$this->add($span);
} else {
$this->add(Html::tag(
@ -75,9 +77,11 @@ class RenderedProcessActionBar extends ActionBar
[
'href' => $url->with('unlocked', true),
'title' => mt('businessprocess', 'Click to unlock editing for this process'),
'class' => 'icon-lock'
],
mt('businessprocess', 'Unlock Editing')
[
new Icon('lock'),
mt('businessprocess', 'Unlock Editing')
]
));
}
} elseif (! $hasChanges) {
@ -86,9 +90,11 @@ class RenderedProcessActionBar extends ActionBar
[
'href' => $url->without('unlocked')->without('action'),
'title' => mt('businessprocess', 'Click to lock editing for this process'),
'class' => 'icon-lock-open'
],
mt('businessprocess', 'Lock Editing')
[
new Icon('lock-open'),
mt('businessprocess', 'Lock Editing')
]
));
}
@ -100,9 +106,11 @@ class RenderedProcessActionBar extends ActionBar
'data-base-target' => '_next',
'href' => Url::fromPath('businessprocess/process/config', $this->currentProcessParams($url)),
'title' => mt('businessprocess', 'Modify this process'),
'class' => 'icon-wrench'
],
mt('businessprocess', 'Config')
[
new Icon('wrench'),
mt('businessprocess', 'Config')
]
));
} else {
$this->add(Html::tag(
@ -113,9 +121,11 @@ class RenderedProcessActionBar extends ActionBar
'editnode' => $url->getParam('node')
])->getAbsoluteUrl(),
'title' => mt('businessprocess', 'Modify this process'),
'class' => 'icon-wrench'
],
mt('businessprocess', 'Config')
[
new Icon('wrench'),
mt('businessprocess', 'Config')
]
));
}
}
@ -126,9 +136,12 @@ class RenderedProcessActionBar extends ActionBar
[
'href' => $url->with('action', 'add'),
'title' => mt('businessprocess', 'Add a new business process node'),
'class' => 'icon-plus button-link'
'class' => 'button-link'
],
mt('businessprocess', 'Add Node')
[
new Icon('plus'),
mt('businessprocess', 'Add Node')
]
));
}
}

View file

@ -12,12 +12,12 @@ use Icinga\Module\Businessprocess\Web\Component\Controls;
use Icinga\Module\Businessprocess\Web\Component\Content;
use Icinga\Module\Businessprocess\Web\Component\Tabs;
use Icinga\Module\Businessprocess\Web\Form\FormLoader;
use Icinga\Web\Controller as ModuleController;
use Icinga\Web\Notification;
use Icinga\Web\View;
use ipl\Html\Html;
use ipl\Web\Compat\CompatController;
class Controller extends ModuleController
class Controller extends CompatController
{
/** @var View */
public $view;
@ -176,14 +176,6 @@ class Controller extends ModuleController
return $this;
}
protected function setTitle($title)
{
$args = func_get_args();
array_shift($args);
$this->view->title = vsprintf($title, $args);
return $this;
}
protected function addTitle($title)
{
$args = func_get_args();
@ -221,6 +213,7 @@ class Controller extends ModuleController
protected function loadBpConfig()
{
$name = $this->params->get('config');
/** @var LegacyStorage $storage */
$storage = $this->storage();
if (! $storage->hasProcess($name)) {
@ -259,7 +252,7 @@ class Controller extends ModuleController
}
/**
* @return LegacyStorage|Storage
* @return LegacyStorage
*/
protected function storage()
{

View file

@ -5,16 +5,25 @@ namespace Icinga\Module\Businessprocess\Web\Form;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Authentication\Auth;
use Icinga\Module\Businessprocess\Storage\LegacyStorage;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\Storage\Storage;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Web\Session\SessionNamespace;
use ipl\Sql\Connection as IcingaDbConnection;
abstract class BpConfigBaseForm extends QuickForm
{
/** @var LegacyStorage */
/** @var Storage */
protected $storage;
/** @var BpConfig */
protected $config;
protected $bp;
/** @var MonitoringBackend|IcingaDbConnection*/
protected $backend;
/** @var SessionNamespace */
protected $session;
protected function listAvailableBackends()
{
@ -28,15 +37,60 @@ abstract class BpConfigBaseForm extends QuickForm
return $keys;
}
public function setStorage(LegacyStorage $storage)
/**
* Set the storage to use
*
* @param Storage $storage
*
* @return $this
*/
public function setStorage(Storage $storage): self
{
$this->storage = $storage;
return $this;
}
public function setProcessConfig(BpConfig $config)
/**
* Set the config to use
*
* @param BpConfig $config
*
* @return $this
*/
public function setProcess(BpConfig $config): self
{
$this->config = $config;
$this->bp = $config;
$this->setBackend($config->getBackend());
return $this;
}
/**
* Set the backend to use
*
* @param MonitoringBackend|IcingaDbConnection $backend
*
* @return $this
*/
public function setBackend($backend): self
{
$this->backend = $backend;
return $this;
}
/**
* Set the session namespace to use
*
* @param SessionNamespace $session
*
* @return $this
*/
public function setSession(SessionNamespace $session): self
{
$this->session = $session;
return $this;
}
@ -69,4 +123,13 @@ abstract class BpConfigBaseForm extends QuickForm
return true;
}
protected function setPreferredDecorators()
{
parent::setPreferredDecorators();
$this->setAttrib('class', $this->getAttrib('class') . ' bp-form');
return $this;
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Element;
use ipl\Html\Attributes;
use ipl\Html\FormElement\FieldsetElement;
class IplStateOverrides extends FieldsetElement
{
/** @var array */
protected $options = [];
/**
* Set the options show
*
* @param array $options
*
* @return $this
*/
public function setOptions(array $options): self
{
$this->options = $options;
return $this;
}
/**
* Get the options to show
*
* @return array
*/
public function getOptions(): array
{
return $this->options;
}
public function getValues()
{
$cleanedValue = parent::getValues();
if (! empty($cleanedValue)) {
foreach ($cleanedValue as $from => $to) {
if ((int) $from === (int) $to) {
unset($cleanedValue[$from]);
}
}
}
return $cleanedValue;
}
protected function assemble()
{
$states = $this->getOptions();
foreach ($states as $state => $label) {
if ($state === 0) {
continue;
}
$this->addElement('select', $state, [
'label' => $label,
'value' => $state,
'options' => [$state => $this->translate('Keep actual state')] + $states
]);
}
}
protected function registerAttributeCallbacks(Attributes $attributes)
{
parent::registerAttributeCallbacks($attributes);
$this->getAttributes()
->registerAttributeCallback('options', null, [$this, 'setOptions']);
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Element;
class StateOverrides extends FormElement
{
public $helper = 'formStateOverrides';
/** @var array The overridable states */
protected $states;
/**
* Set the overridable states
*
* @param array $states
*
* @return $this
*/
public function setStates(array $states)
{
$this->states = $states;
return $this;
}
/**
* Get the overridable states
*
* @return array
*/
public function getStates()
{
return $this->states;
}
public function init()
{
$this->setIsArray(true);
}
public function setValue($value)
{
$cleanedValue = [];
if (! empty($value)) {
foreach ($value as $from => $to) {
if ((int) $from !== (int) $to) {
$cleanedValue[$from] = $to;
}
}
}
return parent::setValue($cleanedValue);
}
}

View file

@ -17,6 +17,8 @@ class FormLoader
$basedir = $module->getFormDir();
$ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Forms\\';
}
$file = null;
if (preg_match('~^[a-z0-9/]+$~i', $name)) {
$parts = preg_split('~/~', $name);
$class = ucfirst(array_pop($parts)) . 'Form';

View file

@ -13,7 +13,7 @@ abstract class QuickBaseForm extends Zend_Form implements ValidHtml
* The Icinga module this form belongs to. Usually only set if the
* form is initialized through the FormLoader
*
* @var Module
* @var ?Module
*/
protected $icingaModule;
@ -123,7 +123,6 @@ abstract class QuickBaseForm extends Zend_Form implements ValidHtml
}
if (array_key_exists('icingaModule', $options)) {
/** @var Module icingaModule */
$this->icingaModule = $options['icingaModule'];
$this->icingaModuleName = $this->icingaModule->getName();
unset($options['icingaModule']);

View file

@ -3,6 +3,7 @@
namespace Icinga\Module\Businessprocess\Web\Form;
use Icinga\Application\Icinga;
use Icinga\Application\Web;
use Icinga\Exception\ProgrammingError;
use Icinga\Web\Notification;
use Icinga\Web\Request;
@ -45,7 +46,7 @@ abstract class QuickForm extends QuickBaseForm
protected $request;
/**
* @var Url
* @var ?Url
*/
protected $successUrl;
@ -251,6 +252,10 @@ abstract class QuickForm extends QuickBaseForm
{
}
/**
* @param $action string|Url
* @return $this
*/
public function setAction($action)
{
if ($action instanceof Url) {
@ -428,14 +433,18 @@ abstract class QuickForm extends QuickBaseForm
protected function redirectAndExit($url)
{
/** @var Web $app */
$app = Icinga::app();
/** @var Response $response */
$response = Icinga::app()->getFrontController()->getResponse();
$response = $app->getFrontController()->getResponse();
$response->redirectAndExit($url);
}
protected function setHttpResponseCode($code)
{
Icinga::app()->getFrontController()->getResponse()->setHttpResponseCode($code);
/** @var Web $app */
$app = Icinga::app();
$app->getFrontController()->getResponse()->setHttpResponseCode($code);
return $this;
}
@ -461,8 +470,10 @@ abstract class QuickForm extends QuickBaseForm
public function getRequest()
{
if ($this->request === null) {
/** @var Web $app */
$app = Icinga::app();
/** @var Request $request */
$request = Icinga::app()->getFrontController()->getRequest();
$request = $app->getFrontController()->getRequest();
$this->setRequest($request);
}
return $this->request;
@ -471,14 +482,15 @@ abstract class QuickForm extends QuickBaseForm
public function hasBeenSent()
{
if ($this->hasBeenSent === null) {
/** @var Request $req */
if ($this->request === null) {
$req = Icinga::app()->getFrontController()->getRequest();
/** @var Web $app */
$app = Icinga::app();
$req = $app->getFrontController()->getRequest();
} else {
$req = $this->request;
}
/** @var Request $req */
if ($req->isPost()) {
$post = $req->getPost();
$this->hasBeenSent = array_key_exists(self::ID, $post) &&

View file

@ -0,0 +1,96 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Validator;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\ServiceNode;
use Icinga\Module\Businessprocess\State\IcingaDbState;
use Icinga\Module\Businessprocess\State\MonitoringState;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use ipl\I18n\Translation;
use ipl\Validator\BaseValidator;
use ipl\Web\FormElement\TermInput\Term;
use LogicException;
class HostServiceTermValidator extends BaseValidator
{
use Translation;
/** @var ?BpNode */
protected $parent;
/**
* Set the affected process
*
* @param BpNode $parent
*
* @return $this
*/
public function setParent(BpNode $parent): self
{
$this->parent = $parent;
return $this;
}
public function isValid($terms)
{
if ($this->parent === null) {
throw new LogicException('Missing parent process. Cannot validate terms.');
}
if (! is_array($terms)) {
$terms = [$terms];
}
$isValid = true;
$testConfig = new BpConfig();
foreach ($terms as $term) {
/** @var Term $term */
[$hostName, $serviceName] = BpConfig::splitNodeName($term->getSearchValue());
if ($serviceName !== null && $serviceName !== 'Hoststatus') {
$node = $testConfig->createService($hostName, $serviceName);
} else {
$node = $testConfig->createHost($hostName);
if ($serviceName === null) {
$term->setSearchValue(BpConfig::joinNodeName($hostName, 'Hoststatus'));
}
}
if ($this->parent->hasChild($term->getSearchValue())) {
$term->setMessage($this->translate('Already defined in this process'));
$isValid = false;
} else {
$testConfig->getNode('__unbound__')
->addChild($node);
}
}
if ($this->parent->getBpConfig()->getBackend() instanceof MonitoringBackend) {
MonitoringState::apply($testConfig);
} else {
IcingaDbState::apply($testConfig);
}
foreach ($terms as $term) {
/** @var Term $term */
$node = $testConfig->getNode($term->getSearchValue());
if ($node->isMissing()) {
if ($node instanceof ServiceNode) {
$term->setMessage($this->translate('Service not found'));
} else {
$term->setMessage($this->translate('Host not found'));
}
$isValid = false;
} else {
$term->setLabel($node->getAlias());
$term->setClass($node->getObjectClassName());
}
}
return $isValid;
}
}

View file

@ -1,57 +0,0 @@
<?php
namespace Icinga\Module\Businessprocess\Web\Form\Validator;
use Icinga\Module\Businessprocess\BpConfig;
use Icinga\Module\Businessprocess\BpNode;
use Icinga\Module\Businessprocess\Forms\EditNodeForm;
use Icinga\Module\Businessprocess\Web\Form\QuickForm;
use Zend_Validate_Abstract;
class NoDuplicateChildrenValidator extends Zend_Validate_Abstract
{
const CHILD_FOUND = 'childFound';
/** @var QuickForm */
protected $form;
/** @var BpConfig */
protected $bp;
/** @var BpNode */
protected $parent;
/** @var string */
protected $label;
public function __construct(QuickForm $form, BpConfig $bp, BpNode $parent = null)
{
$this->form = $form;
$this->bp = $bp;
$this->parent = $parent;
$this->_messageVariables['label'] = 'label';
$this->_messageTemplates = [
self::CHILD_FOUND => mt('businessprocess', '%label% is already defined in this process')
];
}
public function isValid($value)
{
if ($this->parent === null) {
$found = $this->bp->hasRootNode($value);
} elseif ($this->form instanceof EditNodeForm && $this->form->getNode()->getName() === $value) {
$found = false;
} else {
$found = $this->parent->hasChild($value);
}
if (! $found) {
return true;
}
$this->label = $this->form->getElement('children')->getMultiOptions()[$value];
$this->_error(self::CHILD_FOUND);
return false;
}
}

View file

@ -19,12 +19,10 @@ class ProcessProblemsBadge extends BadgeNavigationItemRenderer
public function getCount()
{
$count = 0;
if ($this->count === null) {
$storage = LegacyStorage::getInstance();
$count = 0;
$bp = $storage->loadProcess($this->getBpConfigName());
foreach ($bp->getRootNodes() as $rootNode) {
if (! $rootNode->isEmpty() &&
$rootNode->getState() !== $rootNode::ICINGA_PENDING

View file

@ -3,6 +3,8 @@
namespace Icinga\Module\Businessprocess\Web;
use Icinga\Application\Icinga;
use Icinga\Application\Web;
use Icinga\Web\Request;
use Icinga\Web\Url as WebUrl;
/**
@ -14,13 +16,17 @@ use Icinga\Web\Url as WebUrl;
*/
class Url extends WebUrl
{
/**
* @return FakeRequest|Request
*/
protected static function getRequest()
{
$app = Icinga::app();
if ($app->isCli()) {
return new FakeRequest();
} else {
return $app->getRequest();
}
/** @var Web $app */
return $app->getRequest();
}
}

View file

@ -1,8 +1,8 @@
Name: Businessprocess
Version: 2.4.0
Version: 2.5.0
Requires:
Libraries: icinga-php-library (>=0.8.0), icinga-php-thirdparty (>=0.11.0)
Modules: monitoring (>=2.9.0), icingadb (>=1.0.0)
Libraries: icinga-php-library (>=0.13.0), icinga-php-thirdparty (>=0.12.0)
Modules: monitoring (>=2.9.0), icingadb (>=1.1.0)
Description: A Business Process viewer and modeler
Provides a web-based process modeler for Icinga. It integrates as a module
into Icinga Web 2 and provides a plugin check command for Icinga. Tile and tree

View file

@ -1,18 +0,0 @@
Building Debian packages
========================
This is work in progress, please expect build instructions to change any time
soon. Currently, to build custom Debian or Ubuntu packages, please proceed as
follows:
```sh
apt-get install --no-install-recommends \
debhelper devscripts build-essential fakeroot libparse-debcontrol-perl
# Eventually adjust debian/changelog
cp -a packaging/debian debian
dpkg-buildpackage -us -uc
rm -rf debian
```
Please move to your parent directory (`cd ..`) to find your new Debian packages.

Some files were not shown because too many files have changed in this diff Show more