Merge pull request #1860 from Icinga/feature/apply-exists
Implement AssignFilterHelper to improve apply/assign matching
This commit is contained in:
commit
644853f7a0
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Module\Director\Data;
|
||||
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Data\Filter\FilterAnd;
|
||||
use Icinga\Data\Filter\FilterExpression;
|
||||
use Icinga\Data\Filter\FilterNot;
|
||||
use Icinga\Data\Filter\FilterOr;
|
||||
use Icinga\Exception\NotImplementedError;
|
||||
|
||||
/**
|
||||
* Class ApplyFilterMatches
|
||||
*
|
||||
* A wrapper for Icinga Filter to evaluate filters against Director's objects
|
||||
*/
|
||||
class AssignFilterHelper
|
||||
{
|
||||
/** @var Filter */
|
||||
protected $filter;
|
||||
|
||||
public function __construct(Filter $filter)
|
||||
{
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
*
|
||||
* @return bool
|
||||
* @throws NotImplementedError
|
||||
*/
|
||||
public function matches($object)
|
||||
{
|
||||
return $this->matchesPart($this->filter, $object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Filter $filter
|
||||
* @param object $object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function matchesFilter(Filter $filter, $object)
|
||||
{
|
||||
$helper = new static($filter);
|
||||
return $helper->matches($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Filter $filter
|
||||
* @param object $object
|
||||
*
|
||||
* @return bool
|
||||
* @throws NotImplementedError
|
||||
*/
|
||||
protected function matchesPart(Filter $filter, $object)
|
||||
{
|
||||
if ($filter->isChain()) {
|
||||
return $this->matchesChain($filter, $object);
|
||||
} elseif ($filter->isExpression()) {
|
||||
/** @var FilterExpression $filter */
|
||||
return $this->matchesExpression($filter, $object);
|
||||
} else {
|
||||
return $filter->matches($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Filter $filter
|
||||
* @param object $object
|
||||
*
|
||||
* @return bool
|
||||
* @throws NotImplementedError
|
||||
*/
|
||||
protected function matchesChain(Filter $filter, $object)
|
||||
{
|
||||
if ($filter instanceof FilterAnd) {
|
||||
foreach ($filter->filters() as $f) {
|
||||
if (! $this->matchesPart($f, $object)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} elseif ($filter instanceof FilterOr) {
|
||||
foreach ($filter->filters() as $f) {
|
||||
if ($this->matchesPart($f, $object)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} elseif ($filter instanceof FilterNot) {
|
||||
foreach ($filter->filters() as $f) {
|
||||
if ($this->matchesPart($f, $object)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$class = get_class($filter);
|
||||
$parts = preg_split('~\\~', $class);
|
||||
|
||||
throw new NotImplementedError(
|
||||
'Matching for Filter of type "%s" is not implemented',
|
||||
end($parts)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FilterExpression $filter
|
||||
* @param object $object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function matchesExpression(FilterExpression $filter, $object)
|
||||
{
|
||||
$column = $filter->getColumn();
|
||||
$sign = $filter->getSign();
|
||||
$expression = $filter->getExpression();
|
||||
|
||||
if ($sign === '=') {
|
||||
if ($expression === true) {
|
||||
return property_exists($object, $column) && ! empty($object->{$column});
|
||||
} elseif ($expression === false) {
|
||||
return ! property_exists($object, $column) || empty($object->{$column});
|
||||
} elseif (is_string($expression) && strpos($expression, '*') !== false) {
|
||||
if (! property_exists($object, $column) || empty($object->{$column})) {
|
||||
return false;
|
||||
}
|
||||
$value = $object->{$column};
|
||||
|
||||
$parts = array();
|
||||
foreach (preg_split('~\*~', $expression) as $part) {
|
||||
$parts[] = preg_quote($part);
|
||||
}
|
||||
// match() is case insensitive
|
||||
$pattern = '/^' . implode('.*', $parts) . '$/i';
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $candidate) {
|
||||
if (preg_match($pattern, $candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) preg_match($pattern, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default behavior
|
||||
return $filter->matches($object);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use Icinga\Data\Filter\Filter;
|
|||
use Icinga\Data\Filter\FilterExpression;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Module\Director\Application\MemoryLimit;
|
||||
use Icinga\Module\Director\Data\AssignFilterHelper;
|
||||
use Icinga\Module\Director\Db;
|
||||
use Icinga\Module\Director\Db\Cache\PrefetchCache;
|
||||
use stdClass;
|
||||
|
@ -54,7 +55,7 @@ abstract class ObjectApplyMatches
|
|||
{
|
||||
$filterObj = static::getPreparedFilter($filter);
|
||||
if ($filterObj->isExpression() || ! $filterObj->isEmpty()) {
|
||||
return $filterObj->matches($this->flatObject);
|
||||
return AssignFilterHelper::matchesFilter($filterObj, $this->flatObject);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -72,8 +73,10 @@ abstract class ObjectApplyMatches
|
|||
Benchmark::measure(sprintf('Starting Filter %s', $filter));
|
||||
$filter = clone($filter);
|
||||
static::fixFilterColumns($filter);
|
||||
$helper = new AssignFilterHelper($filter);
|
||||
|
||||
foreach (static::flatObjects($db) as $object) {
|
||||
if ($filter->matches($object)) {
|
||||
if ($helper->matches($object)) {
|
||||
$name = $object->object_name;
|
||||
$result[] = $name;
|
||||
}
|
||||
|
@ -148,6 +151,7 @@ abstract class ObjectApplyMatches
|
|||
public static function fixFilterColumns(Filter $filter)
|
||||
{
|
||||
if ($filter->isExpression()) {
|
||||
/** @var FilterExpression $filter */
|
||||
static::fixFilterExpressionColumn($filter);
|
||||
} else {
|
||||
foreach ($filter->filters() as $sub) {
|
||||
|
@ -164,7 +168,6 @@ abstract class ObjectApplyMatches
|
|||
$filter->setColumn($column);
|
||||
}
|
||||
|
||||
/** @var FilterExpression $filter */
|
||||
$col = $filter->getColumn();
|
||||
$type = static::$type;
|
||||
|
||||
|
|
|
@ -79,9 +79,15 @@ class ApplyRulesTable extends ZfQueryBasedTable
|
|||
}
|
||||
$url = Url::fromPath("director/{$this->baseObjectUrl}/edit", $params);
|
||||
|
||||
$assignWhere = $this->renderApplyFilter($row->assign_filter);
|
||||
|
||||
if (! empty($row->apply_for)) {
|
||||
$assignWhere = sprintf('apply for %s / %s', $row->apply_for, $assignWhere);
|
||||
}
|
||||
|
||||
$tr = static::tr([
|
||||
static::td(Link::create($row->object_name, $url)),
|
||||
static::td($this->renderApplyFilter($row->assign_filter)),
|
||||
static::td($assignWhere),
|
||||
// NOT (YET) static::td($this->createActionLinks($row))->setSeparator(' ')
|
||||
]);
|
||||
|
||||
|
@ -203,6 +209,7 @@ class ApplyRulesTable extends ZfQueryBasedTable
|
|||
'object_name' => 'o.object_name',
|
||||
'disabled' => 'o.disabled',
|
||||
'assign_filter' => 'o.assign_filter',
|
||||
'apply_for' => 'o.apply_for',
|
||||
];
|
||||
$query = $this->db()->select()->from(
|
||||
['o' => $table],
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Icinga\Module\Director\Web\Table;
|
|||
use dipl\Html\Html;
|
||||
use Icinga\Data\DataArray\ArrayDatasource;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Module\Director\IcingaConfig\AssignRenderer;
|
||||
use Icinga\Module\Director\Objects\HostApplyMatches;
|
||||
use Icinga\Module\Director\Objects\IcingaHost;
|
||||
use dipl\Html\Link;
|
||||
|
@ -96,10 +98,16 @@ class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable
|
|||
$link = Html::tag('a', $row->name);
|
||||
}
|
||||
} else {
|
||||
$applyFor = '';
|
||||
if (! empty($row->apply_for)) {
|
||||
$applyFor = sprintf('(apply for %s) ', $row->apply_for);
|
||||
}
|
||||
|
||||
$link = Link::create(sprintf(
|
||||
$this->translate('%s (where %s)'),
|
||||
$this->translate('%s %s(%s)'),
|
||||
$row->name,
|
||||
$row->filter
|
||||
$applyFor,
|
||||
$this->renderApplyFilter($row->filter)
|
||||
), 'director/host/appliedservice', [
|
||||
'name' => $this->host->getObjectName(),
|
||||
'service_id' => $row->id,
|
||||
|
@ -109,6 +117,22 @@ class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable
|
|||
return $this::row([$link], $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Filter $assignFilter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderApplyFilter(Filter $assignFilter)
|
||||
{
|
||||
try {
|
||||
$string = AssignRenderer::forFilter($assignFilter)->renderAssign();
|
||||
} catch (IcingaException $e) {
|
||||
$string = 'Error in Filter rendering: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Icinga\Data\SimpleQuery
|
||||
*/
|
||||
|
@ -130,6 +154,7 @@ class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable
|
|||
'disabled' => 'disabled',
|
||||
'blacklisted' => 'blacklisted',
|
||||
'assign_filter' => 'assign_filter',
|
||||
'apply_for' => 'apply_for',
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -160,6 +185,7 @@ class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable
|
|||
'id' => 's.id',
|
||||
'name' => 's.object_name',
|
||||
'assign_filter' => 's.assign_filter',
|
||||
'apply_for' => 's.apply_for',
|
||||
'disabled' => 's.disabled',
|
||||
'blacklisted' => "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END",
|
||||
]
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Icinga\Module\Director\IcingaConfig;
|
||||
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Module\Director\Data\AssignFilterHelper;
|
||||
use Icinga\Module\Director\Objects\HostApplyMatches;
|
||||
use Icinga\Module\Director\Test\BaseTestCase;
|
||||
|
||||
class AssignFilterHelperTest extends BaseTestCase
|
||||
{
|
||||
protected static $exampleHost;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
self::$exampleHost = (object) [
|
||||
'address' => '127.0.0.1',
|
||||
'vars.operatingsystem' => 'centos',
|
||||
'vars.customer' => 'ACME',
|
||||
'vars.roles' => ['webserver', 'mailserver'],
|
||||
'vars.bool_string' => 'true',
|
||||
'groups' => ['web-server', 'mail-server'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSimpleApplyFilter()
|
||||
{
|
||||
$this->assertFilterOutcome(true, 'host.address=true', self::$exampleHost);
|
||||
$this->assertFilterOutcome(false, 'host.address=false', self::$exampleHost);
|
||||
$this->assertFilterOutcome(true, 'host.address=false', (object) ['address' => null]);
|
||||
$this->assertFilterOutcome(false, 'host.address=true', (object) ['address' => null]);
|
||||
$this->assertFilterOutcome(true, 'host.address=%22127.0.0.%2A%22', self::$exampleHost);
|
||||
}
|
||||
|
||||
public function testListApplyFilter()
|
||||
{
|
||||
$this->assertFilterOutcome(true, 'host.vars.roles=%22*server%22', self::$exampleHost);
|
||||
$this->assertFilterOutcome(true, 'host.groups=%22*-server%22', self::$exampleHost);
|
||||
$this->assertFilterOutcome(false, 'host.groups=%22*-nothing%22', self::$exampleHost);
|
||||
}
|
||||
|
||||
public function testComplexApplyFilter()
|
||||
{
|
||||
$this->assertFilterOutcome(
|
||||
true,
|
||||
'host.vars.operatingsystem=%5B%22centos%22%2C%22fedora%22%5D|host.vars.osfamily=%22redhat%22',
|
||||
self::$exampleHost
|
||||
);
|
||||
|
||||
$this->assertFilterOutcome(
|
||||
false,
|
||||
'host.vars.operatingsystem=%5B%22centos%22%2C%22fedora%22%5D&(!(host.vars.customer=%22acme*%22))',
|
||||
self::$exampleHost
|
||||
);
|
||||
|
||||
$this->assertFilterOutcome(
|
||||
true,
|
||||
'!(host.vars.bool_string="false")&host.vars.operatingsystem="centos"',
|
||||
self::$exampleHost
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $expected
|
||||
* @param string $filterQuery
|
||||
* @param object $object
|
||||
* @param string $message
|
||||
*/
|
||||
protected function assertFilterOutcome($expected, $filterQuery, $object, $message = null, $type = 'host')
|
||||
{
|
||||
$filter = Filter::fromQueryString($filterQuery);
|
||||
|
||||
if ($type === 'host') {
|
||||
HostApplyMatches::fixFilterColumns($filter);
|
||||
}
|
||||
|
||||
$helper = new AssignFilterHelper($filter);
|
||||
$actual = $helper->matches($object);
|
||||
|
||||
if ($message === null) {
|
||||
$message = sprintf('with filter "%s"', $filterQuery);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $actual, $message);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue