From e7bd4343b50e202ce4e8e1f4c53281ae6ee06e87 Mon Sep 17 00:00:00 2001 From: Corentin Ardeois Date: Tue, 6 Sep 2016 11:18:29 -0400 Subject: [PATCH] Add support for Apply for rules in services Apply for rules are defined through `apply_for` property Only Array custom variables will be eligible in apply_for dropdown. API Example: ```bash ./director-curl POST director/service?name=my_service '{"apply_for": "for (checked_tcp_port in host.vars.checked_tcp_ports)" }' ``` Will render as: ``` apply Service "my_service" for (checked_tcp_port in host.vars.checked_tcp_ports) { ... } ``` Feature: https://dev.icinga.org/issues/11976 Depends-On: https://github.com/Icinga/icingaweb2-module-director/pull/20 refs #11976 --- application/forms/IcingaServiceForm.php | 63 ++++++ library/Director/Data/PropertiesFilter.php | 23 +++ .../ArrayCustomVariablesFilter.php | 12 ++ .../CustomVariablesFilter.php | 12 ++ library/Director/Objects/IcingaHost.php | 51 ++--- library/Director/Objects/IcingaService.php | 25 +++ library/Director/Web/Form/QuickBaseForm.php | 4 +- schema/mysql-migrations/upgrade_119.sql | 6 + schema/pgsql-migrations/upgrade_119.sql | 6 + .../Director/Objects/IcingaHostTest.php | 183 ++++++++++++++++++ 10 files changed, 358 insertions(+), 27 deletions(-) create mode 100644 library/Director/Data/PropertiesFilter.php create mode 100644 library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php create mode 100644 library/Director/Data/PropertiesFilter/CustomVariablesFilter.php create mode 100644 schema/mysql-migrations/upgrade_119.sql create mode 100644 schema/pgsql-migrations/upgrade_119.sql diff --git a/application/forms/IcingaServiceForm.php b/application/forms/IcingaServiceForm.php index d138251e..6bafe9d3 100644 --- a/application/forms/IcingaServiceForm.php +++ b/application/forms/IcingaServiceForm.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Forms; +use Icinga\Module\Director\Data\PropertiesFilter\ArrayCustomVariablesFilter; use Icinga\Module\Director\Exception\NestingError; use Icinga\Module\Director\Web\Form\DirectorObjectForm; use Icinga\Module\Director\Objects\IcingaHost; @@ -81,6 +82,7 @@ class IcingaServiceForm extends DirectorObjectForm ->addImportsElement() ->addGroupsElement() ->addDisabledElement() + ->addApplyForElement() ->groupMainProperties() ->addAssignmentElements() ->addCheckCommandElements() @@ -235,6 +237,67 @@ class IcingaServiceForm extends DirectorObjectForm return $this; } + protected function groupMainProperties() + { + $elements = array( + 'imports', + 'object_name', + 'display_name', + 'host_id', + 'address', + 'address6', + 'groups', + 'users', + 'user_groups', + 'apply_to', + 'command_id', // Notification + 'notification_interval', + 'period_id', + 'times_begin', + 'times_end', + 'email', + 'pager', + 'enable_notifications', + 'create_live', + 'disabled', + 'apply_for' + ); + + $this->addDisplayGroup($elements, 'object_definition', array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'dl')), + 'Fieldset', + ), + 'order' => 20, + 'legend' => $this->translate('Main properties') + )); + + return $this; + } + + protected function addApplyForElement() + { + if ($this->object->isApplyRule()) { + $hostProperties = IcingaHost::enumProperties($this->object->getConnection(), 'host.', + new ArrayCustomVariablesFilter()); + $this->addElement('select', 'apply_for', array( + 'label' => $this->translate('Apply For'), + 'class' => 'assign-property autosubmit', + 'multiOptions' => $this->optionalEnum($hostProperties, 'None'), + 'description' => $this->translate( + 'Evaluates the apply for rule for ' . + 'all objects with the custom attribute specified. ' . + 'E.g selecting "host.vars.custom_attr" will generate "for (value in ' . + 'host.vars.array_var)" where "value" will be accessible through "$$value$$". ' . + 'NOTE: only custom variables of type "Array" are eligible.' + ) + )); + + } + return $this; + } + protected function addGroupsElement() { $groups = $this->enumServicegroups(); diff --git a/library/Director/Data/PropertiesFilter.php b/library/Director/Data/PropertiesFilter.php new file mode 100644 index 00000000..5ebf2c60 --- /dev/null +++ b/library/Director/Data/PropertiesFilter.php @@ -0,0 +1,23 @@ +blacklist)); + } +} diff --git a/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php b/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php new file mode 100644 index 00000000..06e56a72 --- /dev/null +++ b/library/Director/Data/PropertiesFilter/ArrayCustomVariablesFilter.php @@ -0,0 +1,12 @@ +datatype); + } +} + diff --git a/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php b/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php new file mode 100644 index 00000000..7a8c7b65 --- /dev/null +++ b/library/Director/Data/PropertiesFilter/CustomVariablesFilter.php @@ -0,0 +1,12 @@ + 'name'); + $hostProperties = array(); + if ($filter === null) { + $filter = new PropertiesFilter(); + } $realProperties = static::create()->listProperties(); sort($realProperties); - $blacklist = array( - 'id', - 'object_name', - 'object_type', - 'disabled', - 'has_agent', - 'master_should_connect', - 'accept_config', - ); - + if ($filter->match(PropertiesFilter::$HOST_PROPERTY, 'name')) { + $hostProperties[$prefix . 'name'] = 'name'; + } foreach ($realProperties as $prop) { - if (in_array($prop, $blacklist)) { + if (!$filter->match(PropertiesFilter::$HOST_PROPERTY, $prop)) { continue; } if (substr($prop, -3) === '_id') { $prop = substr($prop, 0, -3); } - + $hostProperties[$prefix . $prop] = $prop; } @@ -113,14 +110,16 @@ class IcingaHost extends IcingaObject if ($connection !== null) { foreach ($connection->fetchDistinctHostVars() as $var) { - if ($var->datatype) { - $hostVars[$prefix . 'vars.' . $var->varname] = sprintf( - '%s (%s)', - $var->varname, - $var->caption - ); - } else { - $hostVars[$prefix . 'vars.' . $var->varname] = $var->varname; + if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) { + if ($var->datatype) { + $hostVars[$prefix . 'vars.' . $var->varname] = sprintf( + '%s (%s)', + $var->varname, + $var->caption + ); + } else { + $hostVars[$prefix . 'vars.' . $var->varname] = $var->varname; + } } } } @@ -131,9 +130,11 @@ class IcingaHost extends IcingaObject $props = mt('director', 'Host properties'); $vars = mt('director', 'Custom variables'); - $properties = array( - $props => $hostProperties, - ); + + $properties = array(); + if (!empty($hostProperties)) { + $properties[$props] = $hostProperties; + } if (!empty($hostVars)) { $properties[$vars] = $hostVars; diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php index 31eecb4d..c4059757 100644 --- a/library/Director/Objects/IcingaService.php +++ b/library/Director/Objects/IcingaService.php @@ -39,6 +39,7 @@ class IcingaService extends IcingaObject 'icon_image' => null, 'icon_image_alt' => null, 'use_agent' => null, + 'apply_for' => null, 'use_var_overrides' => null, ); @@ -84,6 +85,13 @@ class IcingaService extends IcingaObject protected $prioritizedProperties = array('host_id'); + protected $propertiesNotForRendering = array( + 'id', + 'object_name', + 'object_type', + 'apply_for' + ); + public function getCheckCommand() { $id = $this->getResolvedProperty('check_command_id'); @@ -144,6 +152,23 @@ class IcingaService extends IcingaObject return $this->renderRelationProperty('host', $this->host_id, 'host_name'); } + protected function renderObjectHeader() + { + if ($this->isApplyRule() + && !$this->hasBeenAssignedToHostTemplate() + && $this->get('apply_for') !== null) { + + return sprintf( + "%s %s %s for (value in %s) {\n", + $this->getObjectTypeName(), + $this->getType(), + c::renderString($this->getObjectName()), + $this->get('apply_for') + ); + } + return parent::renderObjectHeader(); + } + protected function renderAssignments() { if (! $this->hasBeenAssignedToHostTemplate()) { diff --git a/library/Director/Web/Form/QuickBaseForm.php b/library/Director/Web/Form/QuickBaseForm.php index a2c4bc40..ee48d024 100644 --- a/library/Director/Web/Form/QuickBaseForm.php +++ b/library/Director/Web/Form/QuickBaseForm.php @@ -72,10 +72,10 @@ abstract class QuickBaseForm extends Zend_Form return $this; } - public function optionalEnum($enum) + public function optionalEnum($enum, $text='- please choose -') { return array( - null => $this->translate('- please choose -') + null => $this->translate($text) ) + $enum; } diff --git a/schema/mysql-migrations/upgrade_119.sql b/schema/mysql-migrations/upgrade_119.sql new file mode 100644 index 00000000..132d3845 --- /dev/null +++ b/schema/mysql-migrations/upgrade_119.sql @@ -0,0 +1,6 @@ +ALTER TABLE icinga_service + ADD COLUMN apply_for VARCHAR(255) DEFAULT NULL AFTER use_agent; + +INSERT INTO director_schema_migration + (schema_version, migration_time) + VALUES (110, NOW()); diff --git a/schema/pgsql-migrations/upgrade_119.sql b/schema/pgsql-migrations/upgrade_119.sql new file mode 100644 index 00000000..6b88334c --- /dev/null +++ b/schema/pgsql-migrations/upgrade_119.sql @@ -0,0 +1,6 @@ +ALTER TABLE icinga_service + ADD COLUMN apply_for character varying(255) DEFAULT NULL; + +INSERT INTO director_schema_migration + (schema_version, migration_time) + VALUES (110, NOW()); diff --git a/test/php/library/Director/Objects/IcingaHostTest.php b/test/php/library/Director/Objects/IcingaHostTest.php index 861bd731..08671d65 100644 --- a/test/php/library/Director/Objects/IcingaHostTest.php +++ b/test/php/library/Director/Objects/IcingaHostTest.php @@ -2,7 +2,10 @@ namespace Tests\Icinga\Module\Director\Objects; +use Icinga\Module\Director\Data\PropertiesFilter\ArrayCustomVariablesFilter; +use Icinga\Module\Director\Data\PropertiesFilter\CustomVariablesFilter; use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDatafield; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaZone; use Icinga\Module\Director\Test\BaseTestCase; @@ -11,6 +14,7 @@ use Icinga\Exception\IcingaException; class IcingaHostTest extends BaseTestCase { protected $testHostName = '___TEST___host'; + protected $testDatafieldName = 'test5'; public function testPropertiesCanBeSet() { @@ -473,6 +477,137 @@ class IcingaHostTest extends BaseTestCase IcingaHost::loadWithApiKey('No___such___key', $db); } + public function testEnumProperties() { + if ($this->skipForMissingDb()) { + return; + } + + $db = $this->getDb(); + $properties = IcingaHost::enumProperties($db); + + $this->assertEquals( + array( + 'Host properties' => $this->getDefaultHostProperties() + ), + $properties + ); + } + + public function testEnumPropertiesWithCustomVars() { + if ($this->skipForMissingDb()) { + return; + } + + $db = $this->getDb(); + + $host = $this->host(); + $host->store($db); + + $properties = IcingaHost::enumProperties($db); + $this->assertEquals( + array( + 'Host properties' => $this->getDefaultHostProperties(), + 'Custom variables' => array( + 'vars.test1' => 'test1', + 'vars.test2' => 'test2', + 'vars.test3' => 'test3', + 'vars.test4' => 'test4' + ) + ), + $properties + ); + } + + public function testEnumPropertiesWithPrefix() { + if ($this->skipForMissingDb()) { + return; + } + + $db = $this->getDb(); + + $host = $this->host(); + $host->store($db); + + $properties = IcingaHost::enumProperties($db, 'host.'); + $this->assertEquals( + array( + 'Host properties' => $this->getDefaultHostProperties('host.'), + 'Custom variables' => array( + 'host.vars.test1' => 'test1', + 'host.vars.test2' => 'test2', + 'host.vars.test3' => 'test3', + 'host.vars.test4' => 'test4' + ) + ), + $properties + ); + } + + public function testEnumPropertiesWithFilter() { + if ($this->skipForMissingDb()) { + return; + } + + $db = $this->getDb(); + + DirectorDatafield::create(array( + 'varname' => $this->testDatafieldName, + 'caption' => 'Blah', + 'description' => '', + 'datatype' => 'Icinga\Module\Director\DataType\DataTypeArray', + 'format' => 'json' + ))->store($db); + + $host = $this->host(); + $host->{'vars.test5'} = array('a', '1'); + $host->store($db); + + $properties = IcingaHost::enumProperties($db, '', new CustomVariablesFilter()); + $this->assertEquals( + array( + 'Custom variables' => array( + 'vars.test1' => 'test1', + 'vars.test2' => 'test2', + 'vars.test3' => 'test3', + 'vars.test4' => 'test4', + 'vars.test5' => 'test5 (Blah)' + ) + ), + $properties + ); + } + + public function testEnumPropertiesWithArrayFilter() { + if ($this->skipForMissingDb()) { + return; + } + + $db = $this->getDb(); + + DirectorDatafield::create(array( + 'varname' => $this->testDatafieldName, + 'caption' => 'Blah', + 'description' => '', + 'datatype' => 'Icinga\Module\Director\DataType\DataTypeArray', + 'format' => 'json' + ))->store($db); + + $host = $this->host(); + $host->{'vars.test5'} = array('a', '1'); + $host->store($db); + + $properties = IcingaHost::enumProperties($db, '', new ArrayCustomVariablesFilter()); + $this->assertEquals( + array( + 'Custom variables' => array( + 'vars.test5' => 'test5 (Blah)' + ) + ), + $properties + ); + } + + protected function getDummyRelatedProperties() { return array( @@ -506,6 +641,36 @@ class IcingaHostTest extends BaseTestCase )); } + protected function getDefaultHostProperties($prefix = '') { + return array( + "${prefix}name" => "name", + "${prefix}action_url" => "action_url", + "${prefix}address" => "address", + "${prefix}address6" => "address6", + "${prefix}api_key" => "api_key", + "${prefix}check_command" => "check_command", + "${prefix}check_interval" => "check_interval", + "${prefix}check_period" => "check_period", + "${prefix}command_endpoint" => "command_endpoint", + "${prefix}display_name" => "display_name", + "${prefix}enable_active_checks" => "enable_active_checks", + "${prefix}enable_event_handler" => "enable_event_handler", + "${prefix}enable_flapping" => "enable_flapping", + "${prefix}enable_notifications" => "enable_notifications", + "${prefix}enable_passive_checks" => "enable_passive_checks", + "${prefix}enable_perfdata" => "enable_perfdata", + "${prefix}event_command" => "event_command", + "${prefix}flapping_threshold" => "flapping_threshold", + "${prefix}icon_image" => "icon_image", + "${prefix}icon_image_alt" => "icon_image_alt", + "${prefix}max_check_attempts" => "max_check_attempts", + "${prefix}notes" => "notes", + "${prefix}notes_url" => "notes_url", + "${prefix}retry_interval" => "retry_interval", + "${prefix}volatile" => "volatile", + "${prefix}zone" => "zone" + ); + } protected function loadRendered($name) { return file_get_contents(__DIR__ . '/rendered/' . $name . '.out'); @@ -528,6 +693,24 @@ class IcingaHostTest extends BaseTestCase IcingaZone::load($name, $db)->delete(); } } + + $this->deleteDatafields(); + } + } + + protected function deleteDatafields() { + $db = $this->getDb(); + $dbAdapter = $db->getDbAdapter(); + $kill = array($this->testDatafieldName); + + foreach ($kill as $name) { + $query = $dbAdapter->select() + ->from('director_datafield') + ->where('varname = ?', $name); + foreach (DirectorDatafield::loadAll($db, $query, 'id') as $datafield) { + $datafield->delete(); + } + } } }