IcingaConfigHelper: granular macro-rendering
fixes #685 fixes #1272 fixes #1482
This commit is contained in:
parent
c5d05454ca
commit
b475aa841e
|
@ -46,7 +46,7 @@ class CustomVariableString extends CustomVariable
|
||||||
public function toConfigString($renderExpressions = false)
|
public function toConfigString($renderExpressions = false)
|
||||||
{
|
{
|
||||||
if ($renderExpressions) {
|
if ($renderExpressions) {
|
||||||
return c::renderStringWithVariables($this->getValue());
|
return c::renderStringWithVariables($this->getValue(), ['config']);
|
||||||
} else {
|
} else {
|
||||||
return c::renderString($this->getValue());
|
return c::renderString($this->getValue());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
namespace Icinga\Module\Director\IcingaConfig;
|
namespace Icinga\Module\Director\IcingaConfig;
|
||||||
|
|
||||||
use Icinga\Exception\IcingaException;
|
use InvalidArgumentException;
|
||||||
use Icinga\Exception\ProgrammingError;
|
|
||||||
|
|
||||||
class IcingaConfigHelper
|
class IcingaConfigHelper
|
||||||
{
|
{
|
||||||
|
@ -11,7 +10,7 @@ class IcingaConfigHelper
|
||||||
* Reserved words according to
|
* Reserved words according to
|
||||||
* https://docs.icinga.com/icinga2/snapshot/doc/module/icinga2/chapter/language-reference#reserved-keywords
|
* https://docs.icinga.com/icinga2/snapshot/doc/module/icinga2/chapter/language-reference#reserved-keywords
|
||||||
*/
|
*/
|
||||||
protected static $reservedWords = array(
|
protected static $reservedWords = [
|
||||||
'object',
|
'object',
|
||||||
'template',
|
'template',
|
||||||
'include',
|
'include',
|
||||||
|
@ -39,7 +38,7 @@ class IcingaConfigHelper
|
||||||
'in',
|
'in',
|
||||||
'current_filename',
|
'current_filename',
|
||||||
'current_line',
|
'current_line',
|
||||||
);
|
];
|
||||||
|
|
||||||
public static function renderKeyValue($key, $value, $prefix = ' ')
|
public static function renderKeyValue($key, $value, $prefix = ' ')
|
||||||
{
|
{
|
||||||
|
@ -69,7 +68,10 @@ class IcingaConfigHelper
|
||||||
} elseif ($value === 'n' || $value === false) {
|
} elseif ($value === 'n' || $value === false) {
|
||||||
return 'false';
|
return 'false';
|
||||||
} else {
|
} else {
|
||||||
throw new ProgrammingError('%s is not a valid boolean', $value);
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
'%s is not a valid boolean',
|
||||||
|
$value
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +100,7 @@ class IcingaConfigHelper
|
||||||
// Parameter? Dedicated method? Always if \n is found?
|
// Parameter? Dedicated method? Always if \n is found?
|
||||||
public static function renderString($string)
|
public static function renderString($string)
|
||||||
{
|
{
|
||||||
$special = array(
|
$special = [
|
||||||
'/\\\/',
|
'/\\\/',
|
||||||
'/"/',
|
'/"/',
|
||||||
'/\$/',
|
'/\$/',
|
||||||
|
@ -106,10 +108,10 @@ class IcingaConfigHelper
|
||||||
'/\r/',
|
'/\r/',
|
||||||
'/\n/',
|
'/\n/',
|
||||||
// '/\b/', -> doesn't work
|
// '/\b/', -> doesn't work
|
||||||
'/\f/'
|
'/\f/',
|
||||||
);
|
];
|
||||||
|
|
||||||
$replace = array(
|
$replace = [
|
||||||
'\\\\\\',
|
'\\\\\\',
|
||||||
'\\"',
|
'\\"',
|
||||||
'\\$',
|
'\\$',
|
||||||
|
@ -118,7 +120,7 @@ class IcingaConfigHelper
|
||||||
'\\n',
|
'\\n',
|
||||||
// '\\b',
|
// '\\b',
|
||||||
'\\f',
|
'\\f',
|
||||||
);
|
];
|
||||||
|
|
||||||
$string = preg_replace($special, $replace, $string);
|
$string = preg_replace($special, $replace, $string);
|
||||||
|
|
||||||
|
@ -144,7 +146,10 @@ class IcingaConfigHelper
|
||||||
} elseif (is_string($value)) {
|
} elseif (is_string($value)) {
|
||||||
return static::renderString($value);
|
return static::renderString($value);
|
||||||
} else {
|
} else {
|
||||||
throw new IcingaException('Unexpected type %s', var_export($value, 1));
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
'Unexpected type %s',
|
||||||
|
var_export($value, 1)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +165,7 @@ class IcingaConfigHelper
|
||||||
// Requires an array
|
// Requires an array
|
||||||
public static function renderArray($array)
|
public static function renderArray($array)
|
||||||
{
|
{
|
||||||
$data = array();
|
$data = [];
|
||||||
foreach ($array as $entry) {
|
foreach ($array as $entry) {
|
||||||
if ($entry instanceof IcingaConfigRenderer) {
|
if ($entry instanceof IcingaConfigRenderer) {
|
||||||
$data[] = $entry;
|
$data[] = $entry;
|
||||||
|
@ -186,7 +191,7 @@ class IcingaConfigHelper
|
||||||
|
|
||||||
public static function renderDictionary($dictionary)
|
public static function renderDictionary($dictionary)
|
||||||
{
|
{
|
||||||
$vals = array();
|
$vals = [];
|
||||||
foreach ($dictionary as $key => $value) {
|
foreach ($dictionary as $key => $value) {
|
||||||
$vals[$key] = rtrim(
|
$vals[$key] = rtrim(
|
||||||
self::renderKeyValue(
|
self::renderKeyValue(
|
||||||
|
@ -259,11 +264,12 @@ class IcingaConfigHelper
|
||||||
$value = 0;
|
$value = 0;
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
|
if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
|
||||||
throw new ProgrammingError(
|
throw new InvalidArgumentException(sprintf(
|
||||||
'"%s" is not a valid time (duration) definition',
|
'"%s" is not a valid time (duration) definition',
|
||||||
$interval
|
$interval
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($m[2]) {
|
switch ($m[2]) {
|
||||||
case 'd':
|
case 'd':
|
||||||
$value += $m[1] * 86400;
|
$value += $m[1] * 86400;
|
||||||
|
@ -290,11 +296,11 @@ class IcingaConfigHelper
|
||||||
return '0s';
|
return '0s';
|
||||||
}
|
}
|
||||||
|
|
||||||
$steps = array(
|
$steps = [
|
||||||
'd' => 86400,
|
'd' => 86400,
|
||||||
'h' => 3600,
|
'h' => 3600,
|
||||||
'm' => 60,
|
'm' => 60,
|
||||||
);
|
];
|
||||||
|
|
||||||
foreach ($steps as $unit => $duration) {
|
foreach ($steps as $unit => $duration) {
|
||||||
if ($seconds % $duration === 0) {
|
if ($seconds % $duration === 0) {
|
||||||
|
@ -305,37 +311,89 @@ class IcingaConfigHelper
|
||||||
return $seconds . 's';
|
return $seconds . 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function stringHasMacro($string)
|
public static function stringHasMacro($string, $macroName = null)
|
||||||
{
|
{
|
||||||
return preg_match('/(?<!\$)\$[\w\.]+\$(?!\$)/', $string);
|
$len = strlen($string);
|
||||||
|
$start = false;
|
||||||
|
// TODO: robust UTF8 support. It works, but it is not 100% correct
|
||||||
|
for ($i = 0; $i < $len; $i++) {
|
||||||
|
if ($string[$i] === '$') {
|
||||||
|
if ($start === false) {
|
||||||
|
$start = $i;
|
||||||
|
} else {
|
||||||
|
// Escaping, $$
|
||||||
|
if ($start + 1 === $i) {
|
||||||
|
$start = false;
|
||||||
|
} else {
|
||||||
|
if ($macroName === null) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if ($macroName === substr($string, $start + 1, $i - $start - 1)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
$start = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function renderStringWithVariables($string)
|
/**
|
||||||
|
* Hint: this isn't complete, but let's restrict ourselves right now
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isValidMacroName($name)
|
||||||
{
|
{
|
||||||
$string = preg_replace(
|
return preg_match('/^[A-z_][A-z_\.\d]+$/', $name)
|
||||||
'/(?<!\$)\$([\w\.]+)\$(?!\$)/',
|
&& ! preg_match('/\.$/', $name);
|
||||||
'" + ${1} + "',
|
}
|
||||||
static::renderString($string)
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: this is an exemption for special variables. It would
|
public static function renderStringWithVariables($string, array $whiteList = null)
|
||||||
// never make any sense to evaluate them at parse time.
|
{
|
||||||
// Another issue remains: there might be other reasons
|
$len = strlen($string);
|
||||||
// for "late evaluation". But how to distinguish those
|
$start = false;
|
||||||
// use cases?
|
$parts = [];
|
||||||
$string = preg_replace(
|
// TODO: UTF8...
|
||||||
'/" \+ ((?:user|notification)\.[\w\.]+) \+ "/',
|
$offset = 0;
|
||||||
'\$${1}\$',
|
for ($i = 0; $i < $len; $i++) {
|
||||||
$string
|
if ($string[$i] === '$') {
|
||||||
);
|
if ($start === false) {
|
||||||
|
$start = $i;
|
||||||
|
} else {
|
||||||
|
// Ignore $$
|
||||||
|
if ($start + 1 === $i) {
|
||||||
|
$start = false;
|
||||||
|
} else {
|
||||||
|
// We got a macro
|
||||||
|
$macroName = substr($string, $start + 1, $i - $start - 1);
|
||||||
|
if (static::isValidMacroName($macroName)) {
|
||||||
|
if ($whiteList === null || in_array($macroName, $whiteList)) {
|
||||||
|
if ($start > $offset) {
|
||||||
|
$parts[] = static::renderString(
|
||||||
|
substr($string, $offset, $start - $offset)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$parts[] = $macroName;
|
||||||
|
$offset = $i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (substr($string, 0, 5) === '"" + ') {
|
$start = false;
|
||||||
$string = substr($string, 5);
|
}
|
||||||
}
|
}
|
||||||
if (substr($string, -5) === ' + ""') {
|
}
|
||||||
$string = substr($string, 0, -5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $string;
|
if ($offset < $i) {
|
||||||
|
$parts[] = static::renderString(substr($string, $offset, $i - $offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' + ', $parts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace Icinga\Module\Director\IcingaConfig;
|
namespace Icinga\Module\Director\IcingaConfig;
|
||||||
|
|
||||||
use Icinga\Exception\ProgrammingError;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
class IcingaConfigRendered implements IcingaConfigRenderer
|
class IcingaConfigRendered implements IcingaConfigRenderer
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ class IcingaConfigRendered implements IcingaConfigRenderer
|
||||||
public function __construct($string)
|
public function __construct($string)
|
||||||
{
|
{
|
||||||
if (! is_string($string)) {
|
if (! is_string($string)) {
|
||||||
throw new ProgrammingError('IcingaConfigRendered accepts only strings');
|
throw new InvalidArgumentException('IcingaConfigRendered accepts only strings');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->rendered = $string;
|
$this->rendered = $string;
|
||||||
|
|
|
@ -15,15 +15,12 @@ class CustomVariablesTest extends BaseTestCase
|
||||||
$vars->bla = 'da';
|
$vars->bla = 'da';
|
||||||
$vars->{'aBc'} = 'normal';
|
$vars->{'aBc'} = 'normal';
|
||||||
$vars->{'a-0'} = 'special';
|
$vars->{'a-0'} = 'special';
|
||||||
$expected = $this->indentVarsList(array(
|
$expected = $this->indentVarsList([
|
||||||
'vars["a-0"] = "special"',
|
'vars["a-0"] = "special"',
|
||||||
'vars.aBc = "normal"',
|
'vars.aBc = "normal"',
|
||||||
'vars.bla = "da"'
|
'vars.bla = "da"'
|
||||||
));
|
]);
|
||||||
$this->assertEquals(
|
$this->assertEquals($expected, $vars->toConfigString());
|
||||||
$vars->toConfigString(),
|
|
||||||
$expected
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testVarsCanBeUnsetAndSetAgain()
|
public function testVarsCanBeUnsetAndSetAgain()
|
||||||
|
@ -33,24 +30,21 @@ class CustomVariablesTest extends BaseTestCase
|
||||||
unset($vars->one);
|
unset($vars->one);
|
||||||
$vars->one = 'three';
|
$vars->one = 'three';
|
||||||
|
|
||||||
$res = array();
|
$res = [];
|
||||||
foreach ($vars as $k => $v) {
|
foreach ($vars as $k => $v) {
|
||||||
$res[$k] = $v->getValue();
|
$res[$k] = $v->getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(['one' => 'three'], $res);
|
||||||
array('one' => 'three'),
|
|
||||||
$res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNumericKeysAreRenderedWithArraySyntax()
|
public function testNumericKeysAreRenderedWithArraySyntax()
|
||||||
{
|
{
|
||||||
$vars = $this->newVars();
|
$vars = $this->newVars();
|
||||||
$vars->{'1'} = 1;
|
$vars->{'1'} = 1;
|
||||||
$expected = $this->indentVarsList(array(
|
$expected = $this->indentVarsList([
|
||||||
'vars["1"] = 1'
|
'vars["1"] = 1'
|
||||||
));
|
]);
|
||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
$expected,
|
$expected,
|
||||||
|
@ -63,14 +57,11 @@ class CustomVariablesTest extends BaseTestCase
|
||||||
$vars = $this->newVars();
|
$vars = $this->newVars();
|
||||||
$vars->bla = 'da';
|
$vars->bla = 'da';
|
||||||
$vars->abc = '$val$';
|
$vars->abc = '$val$';
|
||||||
$expected = $this->indentVarsList(array(
|
$expected = $this->indentVarsList([
|
||||||
'vars.abc = val',
|
'vars.abc = "$val$"',
|
||||||
'vars.bla = "da"'
|
'vars.bla = "da"'
|
||||||
));
|
]);
|
||||||
$this->assertEquals(
|
$this->assertEquals($expected, $vars->toConfigString(true));
|
||||||
$vars->toConfigString(true),
|
|
||||||
$expected
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function indentVarsList($vars)
|
protected function indentVarsList($vars)
|
||||||
|
|
|
@ -19,7 +19,7 @@ class IcingaConfigHelperTest extends BaseTestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @expectedException \Icinga\Exception\ProgrammingError
|
* @expectedException \InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function testWhetherInvalidIntervalStringRaisesException()
|
public function testWhetherInvalidIntervalStringRaisesException()
|
||||||
{
|
{
|
||||||
|
@ -53,12 +53,12 @@ class IcingaConfigHelperTest extends BaseTestCase
|
||||||
|
|
||||||
public function testWhetherDictionaryRendersCorrectly()
|
public function testWhetherDictionaryRendersCorrectly()
|
||||||
{
|
{
|
||||||
$dict = (object) array(
|
$dict = (object) [
|
||||||
'key1' => 'bla',
|
'key1' => 'bla',
|
||||||
'include' => 'reserved',
|
'include' => 'reserved',
|
||||||
'spe cial' => 'value',
|
'spe cial' => 'value',
|
||||||
'0' => 'numeric',
|
'0' => 'numeric',
|
||||||
);
|
];
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
c::renderDictionary($dict),
|
c::renderDictionary($dict),
|
||||||
rtrim($this->loadRendered('dict1'))
|
rtrim($this->loadRendered('dict1'))
|
||||||
|
@ -81,26 +81,50 @@ class IcingaConfigHelperTest extends BaseTestCase
|
||||||
$this->assertEquals(c::renderString('\f'), '"\\\\f"');
|
$this->assertEquals(c::renderString('\f'), '"\\\\f"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMacrosAreDetected()
|
||||||
|
{
|
||||||
|
$this->assertFalse(c::stringHasMacro('$$vars$'));
|
||||||
|
$this->assertFalse(c::stringHasMacro('$$'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('$vars$$'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('$multiple$$vars.nested.name$$vars$ is here'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('some $vars.nested.name$ is here'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('some $vars.nested.name$$vars.even.more$'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('$vars.nested.name$$a$$$$not$'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('MSSQL$$$config$'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('MSSQL$$$config$', 'config'));
|
||||||
|
$this->assertTrue(c::stringHasMacro('MSSQL$$$nix$ and $config$', 'config'));
|
||||||
|
$this->assertFalse(c::stringHasMacro('MSSQL$$$nix$config$ and $$', 'config'));
|
||||||
|
$this->assertFalse(c::stringHasMacro('MSSQL$$$nix$ and $$config$', 'config'));
|
||||||
|
$this->assertFalse(c::stringHasMacro('MSSQL$$$config$', 'conf'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testRenderStringWithVariables()
|
public function testRenderStringWithVariables()
|
||||||
{
|
{
|
||||||
$this->assertEquals(c::renderStringWithVariables('Before $var$'), '"Before " + var');
|
$this->assertEquals('"Before " + var', c::renderStringWithVariables('Before $var$'));
|
||||||
$this->assertEquals(c::renderStringWithVariables('$var$ After'), 'var + " After"');
|
$this->assertEquals(c::renderStringWithVariables('$var$ After'), 'var + " After"');
|
||||||
$this->assertEquals(c::renderStringWithVariables('$var$'), 'var');
|
$this->assertEquals(c::renderStringWithVariables('$var$'), 'var');
|
||||||
$this->assertEquals(c::renderStringWithVariables('$$var$$'), '"$$var$$"');
|
$this->assertEquals(c::renderStringWithVariables('$$var$$'), '"$$var$$"');
|
||||||
$this->assertEquals(c::renderStringWithVariables('Before $$var$$ After'), '"Before $$var$$ After"');
|
$this->assertEquals(c::renderStringWithVariables('Before $$var$$ After'), '"Before $$var$$ After"');
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
c::renderStringWithVariables('Before $name$ $name$ After'),
|
'"Before " + name1 + " " + name2 + " After"',
|
||||||
'"Before " + name + " " + name + " After"'
|
c::renderStringWithVariables('Before $name1$ $name2$ After')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRenderStringWithVariablesX()
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
'"Before " + var1 + " " + var2 + " After"',
|
||||||
|
c::renderStringWithVariables('Before $var1$ $var2$ After')
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
c::renderStringWithVariables('Before $var1$ $var2$ After'),
|
'host.vars.custom',
|
||||||
'"Before " + var1 + " " + var2 + " After"'
|
c::renderStringWithVariables('$host.vars.custom$')
|
||||||
);
|
);
|
||||||
$this->assertEquals(c::renderStringWithVariables('$host.vars.custom$'), 'host.vars.custom');
|
$this->assertEquals('"$var\"$"', c::renderStringWithVariables('$var"$'));
|
||||||
$this->assertEquals(c::renderStringWithVariables('$var"$'), '"$var\"$"');
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
c::renderStringWithVariables('\tI am\rrendering\nproperly\fand I $support$ "multiple" $variables$\$'),
|
'"\\\\tI am\\\\rrendering\\\\nproperly\\\\fand I " + support + " \"multiple\" " + variables + "\\\\$"',
|
||||||
'"\\\\tI am\\\\rrendering\\\\nproperly\\\\fand I " + support + " \"multiple\" " + variables + "\\\\$"'
|
c::renderStringWithVariables('\tI am\rrendering\nproperly\fand I $support$ "multiple" $variables$\$')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue