diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php
index 1d87305..6bfe392 100644
--- a/application/forms/AddNodeForm.php
+++ b/application/forms/AddNodeForm.php
@@ -117,6 +117,7 @@ class AddNodeForm extends QuickForm
'&' => $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'),
diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php
index 588c6de..e97ae59 100644
--- a/application/forms/EditNodeForm.php
+++ b/application/forms/EditNodeForm.php
@@ -114,6 +114,7 @@ class EditNodeForm extends QuickForm
'&' => $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'),
diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php
index 4cc3bcf..0e0b853 100644
--- a/application/forms/ProcessForm.php
+++ b/application/forms/ProcessForm.php
@@ -61,6 +61,7 @@ class ProcessForm extends QuickForm
'&' => $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'),
diff --git a/doc/09-Operators.md b/doc/09-Operators.md
index 53613f2..58db97c 100644
--- a/doc/09-Operators.md
+++ b/doc/09-Operators.md
@@ -17,6 +17,15 @@ The `OR` operator selects the **BEST** state of its child nodes:

+## DEGRADED
+
+The `DEGRADED` operator behaves like an `AND`, but if the resulting
+state is **CRITICAL** it transforms it into a **WARNING**.
+Refer to the table below for the case-by-case
+analysis of the statuses.
+
+
+
## MIN n
The `MIN` operator selects the **WORST** state out of the **BEST n** child node states:
diff --git a/doc/screenshot/09_operators/0905_deg-operator.jpg b/doc/screenshot/09_operators/0905_deg-operator.jpg
new file mode 100644
index 0000000..9dc05a3
Binary files /dev/null and b/doc/screenshot/09_operators/0905_deg-operator.jpg differ
diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php
index 58a7c11..6944f09 100644
--- a/library/Businessprocess/BpNode.php
+++ b/library/Businessprocess/BpNode.php
@@ -11,6 +11,7 @@ class BpNode extends Node
const OP_AND = '&';
const OP_OR = '|';
const OP_NOT = '!';
+ const OP_DEGRADED = '%';
protected $operator = '&';
protected $url;
@@ -289,6 +290,7 @@ class BpNode extends Node
case self::OP_AND:
case self::OP_OR:
case self::OP_NOT:
+ case self::OP_DEGRADED:
return;
default:
if (is_numeric($operator)) {
@@ -460,6 +462,15 @@ class BpNode extends Node
case self::OP_OR:
$sort_state = min($sort_states);
break;
+ case self::OP_DEGRADED:
+ $maxState = max($sort_states);
+ $flags = $maxState & 0xf;
+
+ $maxIcingaState = $this->sortStateTostate($maxState);
+ $warningState = ($this->stateToSortState(self::ICINGA_WARNING) << self::SHIFT_FLAGS) + $flags;
+
+ $sort_state = ($maxIcingaState === self::ICINGA_CRITICAL) ? $warningState : $maxState;
+ break;
default:
// MIN:
$sort_state = 3 << self::SHIFT_FLAGS;
@@ -624,6 +635,9 @@ class BpNode extends Node
case self::OP_NOT:
return 'NOT';
break;
+ case self::OP_DEGRADED:
+ return 'DEG';
+ break;
default:
// MIN
$this->assertNumericOperator();
diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php
index 4142731..17fc8a5 100644
--- a/library/Businessprocess/Storage/LegacyConfigParser.php
+++ b/library/Businessprocess/Storage/LegacyConfigParser.php
@@ -322,7 +322,7 @@ class LegacyConfigParser
}
$op = '&';
- if (preg_match_all('~(?hasNode($val)) {
$node->addChild($bp->getNode($val));
diff --git a/library/Businessprocess/Storage/LegacyConfigRenderer.php b/library/Businessprocess/Storage/LegacyConfigRenderer.php
index ebe9589..7e2e0b2 100644
--- a/library/Businessprocess/Storage/LegacyConfigRenderer.php
+++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php
@@ -186,7 +186,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 !== '&') {
diff --git a/test/php/library/Businessprocess/BpNodeTest.php b/test/php/library/Businessprocess/BpNodeTest.php
index c1f224b..c3da723 100644
--- a/test/php/library/Businessprocess/BpNodeTest.php
+++ b/test/php/library/Businessprocess/BpNodeTest.php
@@ -7,22 +7,20 @@ use Icinga\Module\Businessprocess\Test\BaseTestCase;
class BpNodeTest extends BaseTestCase
{
- /**
- * @expectedException \Icinga\Module\Businessprocess\Exception\NestingError
- */
public function testThrowsNestingErrorWhenCheckedForLoops()
{
+ $this->expectException(\Icinga\Module\Businessprocess\Exception\NestingError::class);
+
/** @var BpNode $bpNode */
$bpNode = $this->makeLoop()->getNode('d');
$bpNode->checkForLoops();
}
- /**
- * @expectedExceptionMessage d -> a -> b -> c -> a
- * @expectedException \Icinga\Module\Businessprocess\Exception\NestingError
- */
public function testNestingErrorReportsFullLoop()
{
+ $this->expectException(\Icinga\Module\Businessprocess\Exception\NestingError::class);
+ $this->expectExceptionMessage('d -> a -> b -> c -> a');
+
/** @var BpNode $bpNode */
$bpNode = $this->makeLoop()->getNode('d');
$bpNode->checkForLoops();
diff --git a/test/php/library/Businessprocess/HostNodeTest.php b/test/php/library/Businessprocess/HostNodeTest.php
index 9839959..ef4155d 100644
--- a/test/php/library/Businessprocess/HostNodeTest.php
+++ b/test/php/library/Businessprocess/HostNodeTest.php
@@ -41,11 +41,9 @@ class HostNodeTest extends BaseTestCase
);
}
- /**
- * @expectedException \Icinga\Exception\ProgrammingError
- */
public function testSettingAnInvalidStateFails()
{
+ $this->expectException(\Icinga\Exception\ProgrammingError::class);
$bp = new BpConfig();
$host = $bp->createHost('localhost')->setState(98);
$bp->createBp('p')->addChild($host)->getState();
diff --git a/test/php/library/Businessprocess/Operators/DegradedOperatorTest.php b/test/php/library/Businessprocess/Operators/DegradedOperatorTest.php
new file mode 100644
index 0000000..72ed5e5
--- /dev/null
+++ b/test/php/library/Businessprocess/Operators/DegradedOperatorTest.php
@@ -0,0 +1,159 @@
+emptyConfigSection());
+ $expressions = [
+ 'a = b;c',
+ 'a = b;c % c;d % d;e',
+ ];
+
+ foreach ($expressions as $expression) {
+ $this->assertInstanceOf(
+ 'Icinga\\Module\\Businessprocess\\BpConfig',
+ $storage->loadFromString('dummy', $expression)
+ );
+ }
+ }
+
+ public function testThreeTimesCriticalIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesCriticalAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testCriticalAndWarningAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testUnknownAndWarningAndOkIsUnknown()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'UNKNOWN',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesWarningAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testUnknownAndWarningAndCriticalIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesOkIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testSimpleDegOperationWorksCorrectly()
+ {
+ $bp = new BpConfig();
+ $bp->throwErrors();
+ $host = $bp->createHost('localhost')->setState(0);
+ $service = $bp->createService('localhost', 'ping')->setState(2);
+ $p = $bp->createBp('p');
+ $p->setOperator('%');
+ $p->addChild($host);
+ $p->addChild($service);
+
+ $this->assertEquals(
+ 'UP',
+ $host->getStateName()
+ );
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $service->getStateName()
+ );
+
+ $this->assertEquals(
+ 'WARNING',
+ $p->getStateName()
+ );
+ }
+
+ /**
+ * @return BpConfig
+ */
+ protected function getBp()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = 'a = b;c % c;d % d;e';
+ $bp = $storage->loadFromString('dummy', $expression);
+ $bp->createBp('b');
+ $bp->createBp('c');
+ $bp->createBp('d');
+
+ return $bp;
+ }
+}