ImportRowModifier: filters, CIDR support

fixes #2756
fixes #2757
This commit is contained in:
Thomas Gelf 2023-05-25 15:44:56 +02:00
parent ac73affcb4
commit 722499ea76
10 changed files with 191 additions and 11 deletions

View File

@ -64,6 +64,41 @@ class ImportRowModifierForm extends DirectorObjectForm
$error = $e->getMessage();
$mods = $this->optionalEnum([]);
}
$this->addElement('YesNo', 'use_filter', [
'label' => $this->translate('Set based on filter'),
'ignore' => true,
'class' => 'autosubmit',
'required' => true,
]);
if ($this->hasBeenSent()) {
$useFilter = $this->getSentValue('use_filter');
if ($useFilter === null) {
$this->setElementValue('use_filter', $useFilter = 'n');
}
} elseif ($object = $this->getObject()) {
$expression = $object->get('filter_expression');
$useFilter = ($expression === null || strlen($expression) === 0) ? 'n' : 'y';
$this->setElementValue('use_filter', $useFilter);
} else {
$this->setElementValue('use_filter', $useFilter = 'n');
}
if ($useFilter === 'y') {
$this->addElement('text', 'filter_expression', [
'label' => $this->translate('Filter Expression'),
'description' => $this->translate(
'This allows to filter for specific parts within the given source expression.'
. ' You are allowed to refer all imported columns. Examples: host=www* would'
. ' set this property only for rows imported with a host property starting'
. ' with "www". Complex example: host=www*&!(address=127.*|address6=::1).'
. ' Please note, that CIDR notation based matches are also supported: '
. ' address=192.0.2.128/25|address=2001:db8::/32|address=::ffff:192.0.2.0/96'
),
'required' => true,
// TODO: validate filter
]);
}
$this->addElement('select', 'provider_class', [
'label' => $this->translate('Modifier'),

View File

@ -23,6 +23,8 @@ This version hasn't been released yet
### Import and Sync
* FEATURE: regular expression based modifier allows explicit NULL on no match (#2705)
* FEATURE: property modifiers can now be applied based on filters (#2756)
* FEATURE: CIDR notation (network ranges) is supported in such filters (#2757)
* FIX: synchronizing Service (and -Set) Templates has been fixed (#2745, #2217)
### Permissions and Restrictions

View File

@ -0,0 +1,90 @@
<?php
namespace Icinga\Module\Director\Filter;
use Icinga\Data\Filter\FilterExpression;
use InvalidArgumentException;
use function array_map;
use function filter_var;
use function inet_pton;
use function pack;
use function preg_match;
use function str_pad;
use function str_split;
class CidrExpression extends FilterExpression
{
protected $networkAddress;
protected $broadcastAddress;
public function __construct($column, $sign, $expression)
{
if ($parts = static::splitOptionalCidrString($expression)) {
list($this->networkAddress, $this->broadcastAddress) = $parts;
} else {
throw new InvalidArgumentException("'$expression' isn't valid CIDR notation");
}
parent::__construct($column, $sign, $expression);
}
public static function isCidrFormat(string $string): bool
{
return static::splitOptionalCidrString($string) !== null;
}
protected static function splitOptionalCidrString(string $string): ?array
{
if (preg_match('#^(.+?)/(\d{1,3})$#', $string, $match)) {
$address = $match[1];
$mask = (int) $match[2];
if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $mask <= 32) {
$bits = 32;
} elseif (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && $mask <= 128) {
$bits = 128;
} else {
return null;
}
$binaryAddress = inet_pton($address);
$broadcast = $binaryAddress | static::bitmaskToInverseBinaryMask($mask, $bits);
return [$binaryAddress, $broadcast];
}
return null;
}
public function matches($row): bool
{
if (! isset($row->{$this->column})) {
return false;
}
$value = inet_pton((string) $row->{$this->column});
return $value >= $this->networkAddress && $value <= $this->broadcastAddress;
}
public static function fromExpression(FilterExpression $filter): CidrExpression
{
$sign = $filter->getSign();
if ($sign !== '=') {
throw new InvalidArgumentException("'$sign' cannot be applied to CIDR notation");
}
return new CidrExpression($filter->getColumn(), $sign, $filter->getExpression());
}
protected static function bitmaskToInverseBinaryMask($mask, $maxLen): string
{
$binary = str_pad(str_pad('', $mask, '0'), $maxLen, '1');
$address = '';
foreach (array_map('bindec', str_split($binary, 8)) as $char) {
$address .= pack('C*', $char);
}
return $address;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Icinga\Module\Director\Filter;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
class FilterEnrichment
{
public static function enrichFilter(Filter $filter): Filter
{
if ($filter instanceof FilterExpression) {
if (CidrExpression::isCidrFormat($filter->getExpression())) {
return CidrExpression::fromExpression($filter);
}
} elseif ($filter instanceof FilterChain) {
foreach ($filter->filters() as $subFilter) {
if ($subFilter instanceof FilterExpression
&& CidrExpression::isCidrFormat($subFilter->getExpression())
) {
$filter->replaceById($subFilter->getId(), CidrExpression::fromExpression($subFilter));
}
}
}
return $filter;
}
}

View File

@ -18,13 +18,14 @@ class ImportRowModifier extends DbObjectWithSettings implements InstantiatedViaH
protected $autoincKeyName = 'id';
protected $defaultProperties = [
'id' => null,
'source_id' => null,
'property_name' => null,
'provider_class' => null,
'target_property' => null,
'priority' => null,
'description' => null,
'id' => null,
'source_id' => null,
'property_name' => null,
'provider_class' => null,
'target_property' => null,
'filter_expression' => null,
'priority' => null,
'description' => null,
];
protected $settingsTable = 'import_row_modifier_setting';

View File

@ -3,12 +3,14 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Application\Benchmark;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Application\MemoryLimit;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
use Icinga\Module\Director\Exception\DuplicateKeyException;
use Icinga\Module\Director\Filter\FilterEnrichment;
use Icinga\Module\Director\Hook\PropertyModifierHook;
use Icinga\Module\Director\Import\Import;
use Icinga\Module\Director\Import\SyncUtils;
@ -252,10 +254,14 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface
foreach ($modifiers as $modPair) {
/** @var PropertyModifierHook $modifier */
list($property, $modifier) = $modPair;
/** @var ?Filter $filter */
list($property, $modifier, $filter) = $modPair;
$rejected = [];
$newRows = [];
foreach ($data as $key => $row) {
if ($filter && ! $filter->matches($row)) {
continue;
}
$this->applyPropertyModifierToRow($modifier, $property, $row);
if ($modifier->rejectsRow()) {
$rejected[] = $key;
@ -372,7 +378,12 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface
{
$mods = [];
foreach ($this->fetchRowModifiers() as $mod) {
$mods[] = [$mod->get('property_name'), $mod->getInstance()];
if ($filterExpression = $mod->get('filter_expression')) {
$filter = FilterEnrichment::enrichFilter(Filter::fromQueryString($filterExpression));
} else {
$filter = null;
}
$mods[] = [$mod->get('property_name'), $mod->getInstance(), $filter];
}
return $mods;

View File

@ -0,0 +1,5 @@
ALTER TABLE import_row_modifier ADD COLUMN filter_expression TEXT DEFAULT NULL AFTER priority;
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (187, NOW());

View File

@ -1459,6 +1459,7 @@ CREATE TABLE import_row_modifier (
target_property VARCHAR(255) DEFAULT NULL,
provider_class VARCHAR(128) NOT NULL,
priority SMALLINT UNSIGNED NOT NULL,
filter_expression TEXT DEFAULT NULL,
description TEXT DEFAULT NULL,
PRIMARY KEY (id),
KEY search_idx (property_name),
@ -2445,4 +2446,4 @@ CREATE TABLE branched_icinga_dependency (
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (186, NOW());
VALUES (187, NOW());

View File

@ -0,0 +1,5 @@
ALTER TABLE import_row_modifier ADD COLUMN filter_expression text DEFAULT NULL,
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (187, NOW());

View File

@ -1608,6 +1608,7 @@ CREATE TABLE import_row_modifier (
target_property character varying(255) DEFAULT NULL,
provider_class character varying(128) NOT NULL,
priority integer NOT NULL,
filter_expression text DEFAULT NULL,
description text DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT row_modifier_import_source
@ -2784,4 +2785,4 @@ CREATE INDEX branched_dependency_search_object_name ON branched_icinga_dependenc
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (186, NOW());
VALUES (187, NOW());