Merge branch 'feature/assign-for-11976'

This commit is contained in:
Thomas Gelf 2016-10-22 06:55:43 +00:00
commit a735df89a0
26 changed files with 506 additions and 35 deletions

View File

@ -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;
@ -47,7 +48,7 @@ class IcingaServiceForm extends DirectorObjectForm
if (!$this->isNew() && $this->host === null) {
$this->host = $this->object->getResolvedRelated('host');
}
} catch(NestingError $nestingError) {
} catch (NestingError $nestingError) {
// ignore for the form to load
}
@ -81,6 +82,7 @@ class IcingaServiceForm extends DirectorObjectForm
->addImportsElement()
->addGroupsElement()
->addDisabledElement()
->addApplyForElement()
->groupMainProperties()
->addAssignmentElements()
->addCheckCommandElements()
@ -210,7 +212,7 @@ class IcingaServiceForm extends DirectorObjectForm
{
$this->addElement('text', 'object_name', array(
'label' => $this->translate('Name'),
'required' => true,
'required' => !$this->object()->isApplyRule(),
'description' => $this->translate(
'Name for the Icinga service you are going to create'
)
@ -235,6 +237,32 @@ class IcingaServiceForm extends DirectorObjectForm
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, $this->translate('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 (config in ' .
'host.vars.array_var)" where "config" will be accessible through "$config$". ' .
'NOTE: only custom variables of type "Array" are eligible.'
)
));
}
return $this;
}
protected function addGroupsElement()
{
$groups = $this->enumServicegroups();

View File

@ -0,0 +1,44 @@
<a id="Service-apply-for-example"></a>Working with Apply for rules - tcp ports example
==============================================
This example wants to show you how to make use of `Apply For` rule for services.
First you need to define a `tcp_ports` data field of type `Array` assigned to a `Host Template`.
Refer to [Working with fields](14-Fields-example-interfaces-array.md) section to setup a data field.
You also need to define a `tcp_port` data field of type `String`, we will associate it to a
`Service Template` later.
Then, please got to the `Dashboard` and choose the `Monitored services` dashlet:
![Dashboard - Monitored services](screenshot/director/15_apply-for-services/151_monitored_services.png)
Then create a new `Service template` with check command `tcp`:
![Define service template - tcp](screenshot/director/15_apply-for-services/152_add_service_template.png)
Then associate the data field `tcp_port` to this `Service template`:
![Associate field to service template - tcp_port](screenshot/director/15_apply-for-services/153_add_service_template_field.png)
Then create a new `apply-rule` for the `Service template`:
![Define apply rule](screenshot/director/15_apply-for-services/154_create_apply_rule.png)
Now define the `Apply For` property, select the previously defined field `tcp_ports` associated to
the host template. `Apply For` rule define a variable `config` that can be used as `$config$`, it
corresponds to the item of the array it will iterate on.
Set the `Tcp port` property to `$config$`:
![Add field to template](screenshot/director/15_apply-for-services/155_configure_apply_for.png)
(Side note: if you can't see your `tcp_ports` property in `Apply For` dropdown, try to create one
host with a non-empty `tcp_ports` value.)
That's it, now all your hosts defining a `tcp_ports` variable will be assigned the `Tcp Check`
service.
Have a look at the config preview, it will show you how `Apply For` services will look like once
deployed:
![Host config preview with Array](screenshot/director/15_apply-for-services/156_config_preview.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,24 @@
<?php
namespace Icinga\Module\Director\Data;
class PropertiesFilter
{
public static $CUSTOM_PROPERTY = 'CUSTOM_PROPERTY';
public static $HOST_PROPERTY = 'HOST_PROPERTY';
protected $blacklist = array(
'id',
'object_name',
'object_type',
'disabled',
'has_agent',
'master_should_connect',
'accept_config',
);
public function match($type, $name, $object = null)
{
return ($type != self::$HOST_PROPERTY || !in_array($name, $this->blacklist));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Icinga\Module\Director\Data\PropertiesFilter;
class ArrayCustomVariablesFilter extends CustomVariablesFilter
{
public function match($type, $name, $object = null)
{
return parent::match($type, $name, $object)
&& $object !== null
&& preg_match('/DataTypeArray[\w]*$/', $object->datatype);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Icinga\Module\Director\Data\PropertiesFilter;
use Icinga\Module\Director\Data\PropertiesFilter;
class CustomVariablesFilter extends PropertiesFilter
{
public function match($type, $name, $object = null)
{
return parent::match($type, $name, $object) && $type === self::$CUSTOM_PROPERTY;
}
}

View File

@ -258,6 +258,11 @@ class IcingaConfigHelper
return $seconds . 's';
}
public static function stringHasMacro($string)
{
return preg_match('/(?<!\$)\$[\w\.]+\$(?!\$)/', $string);
}
public static function renderStringWithVariables($string)
{
$string = preg_replace(

View File

@ -29,6 +29,20 @@ class DirectorActivityLog extends DbObject
'parent_checksum' => null,
);
/**
* @codingStandardsIgnoreStart
*/
protected function setObject_Name($name)
{
// @codingStandardsIgnoreEnd
if ($name === null) {
$name = '';
}
return $this->reallySet('object_name', $name);
}
protected static function username()
{
if (Icinga::app()->isCli()) {

View File

@ -4,6 +4,7 @@ namespace Icinga\Module\Director\Objects;
use Icinga\Data\Db\DbConnection;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Data\PropertiesFilter;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\IcingaConfig\IcingaConfig;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
@ -81,24 +82,23 @@ class IcingaHost extends IcingaObject
protected $supportsFields = true;
public static function enumProperties(DbConnection $connection = null, $prefix = '')
{
$hostProperties = array($prefix . 'name' => 'name');
public static function enumProperties(
DbConnection $connection = null,
$prefix = '',
$filter = null
) {
$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;
}
@ -113,14 +113,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 +133,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;

View File

@ -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');
@ -125,6 +133,20 @@ class IcingaService extends IcingaObject
return $this;
}
/**
* @codingStandardsIgnoreStart
*/
protected function setObject_Name($name)
{
// @codingStandardsIgnoreEnd
if ($name === null && $this->isApplyRule()) {
$name = '';
}
return $this->reallySet('object_name', $name);
}
/**
* Render host_id as host_name
*
@ -144,6 +166,33 @@ 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) {
$name = $this->getObjectName();
$extraName = '';
if (c::stringHasMacro($name)) {
$extraName = c::renderKeyValue('name', c::renderStringWithVariables($name));
$name = '';
} elseif ($name !== '') {
$name = ' ' . c::renderString($name);
}
return sprintf(
"%s %s%s for (config in %s) {\n",
$this->getObjectTypeName(),
$this->getType(),
$name,
$this->get('apply_for')
) . $extraName;
}
return parent::renderObjectHeader();
}
protected function renderAssignments()
{
if (! $this->hasBeenAssignedToHostTemplate()) {

View File

@ -311,6 +311,7 @@ abstract class DirectorObjectForm extends QuickForm
'email',
'pager',
'enable_notifications',
'apply_for',
'create_live',
'disabled',
);

View File

@ -72,11 +72,13 @@ abstract class QuickBaseForm extends Zend_Form
return $this;
}
public function optionalEnum($enum)
public function optionalEnum($enum, $nullLabel = null)
{
return array(
null => $this->translate('- please choose -')
) + $enum;
if ($nullLabel === null) {
$nullLabel = $this->translate('- please choose -');
}
return array(null => $nullLabel) + $enum;
}
protected function handleOptions($options = null)

View File

@ -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 (119, NOW());

View File

@ -557,6 +557,7 @@ CREATE TABLE icinga_service (
icon_image VARCHAR(255) DEFAULT NULL,
icon_image_alt VARCHAR(255) DEFAULT NULL,
use_agent ENUM('y', 'n') DEFAULT NULL,
apply_for VARCHAR(255) DEFAULT NULL,
use_var_overrides ENUM('y', 'n') DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY object_key (object_name, host_id),
@ -1415,4 +1416,4 @@ CREATE TABLE sync_run (
INSERT INTO director_schema_migration
SET migration_time = NOW(),
schema_version = 117;
schema_version = 119;

View File

@ -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 (119, NOW());

View File

@ -700,6 +700,7 @@ CREATE TABLE icinga_service (
icon_image character varying(255) DEFAULT NULL,
icon_image_alt character varying(255) DEFAULT NULL,
use_agent enum_boolean DEFAULT NULL,
apply_for character varying(255) DEFAULT NULL,
use_var_overrides enum_boolean DEFAULT NULL,
PRIMARY KEY (id),
-- UNIQUE INDEX object_name (object_name, zone_id),
@ -1646,4 +1647,4 @@ CREATE UNIQUE INDEX notification_inheritance ON icinga_notification_inheritance
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (117, NOW());
VALUES (119, NOW());

View File

@ -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();
}
}
}
}

View File

@ -244,15 +244,14 @@ class IcingaServiceTest extends BaseTestCase
$db = $this->getDb();
$service = $this->service('___TEST___service_$not_replaced$');
$service->setConnection($db);
$service->object_type = 'apply';
$service->display_name = 'Service: $host.vars.replaced$';
$service->assignments = array(
'host.address="127.*"',
);
$service->{'vars.custom_var'} = '$host.vars.replaced$';
$service->store($db);
$service = IcingaService::loadWithAutoIncId($service->id, $db);
$this->assertEquals(
$this->loadRendered('service3'),
(string) $service
@ -280,6 +279,38 @@ class IcingaServiceTest extends BaseTestCase
);
}
public function testApplyForRendersInVariousModes()
{
if ($this->skipForMissingDb()) {
return;
}
$db = $this->getDb();
$service = $this->service()->setConnection($db);
$service->object_type = 'apply';
$service->apply_for = 'host.vars.test1';
$service->assignments = array(
'host.vars.env="test"'
);
$this->assertEquals(
$this->loadRendered('service5'),
(string) $service
);
$service->object_name = '___TEST$config$___service $host.var.bla$';
$this->assertEquals(
$this->loadRendered('service6'),
(string) $service
);
$service->object_name = '';
$this->assertEquals(
$this->loadRendered('service7'),
(string) $service
);
}
protected function host()
{
return IcingaHost::create(array(

View File

@ -0,0 +1,15 @@
apply Service "___TEST___service" for (config in host.vars.test1) {
display_name = "Whatever service"
vars.test1 = "string"
vars.test2 = 17
vars.test3 = false
vars.test4 = {
a = [ "dict", "ionary" ]
@this = "is"
}
assign where host.vars.env == "test"
import DirectorOverrideTemplate
}

View File

@ -0,0 +1,16 @@
apply Service for (config in host.vars.test1) {
name = "___TEST" + config + "___service " + host.var.bla
display_name = "Whatever service"
vars.test1 = "string"
vars.test2 = 17
vars.test3 = false
vars.test4 = {
a = [ "dict", "ionary" ]
@this = "is"
}
assign where host.vars.env == "test"
import DirectorOverrideTemplate
}

View File

@ -0,0 +1,15 @@
apply Service for (config in host.vars.test1) {
display_name = "Whatever service"
vars.test1 = "string"
vars.test2 = 17
vars.test3 = false
vars.test4 = {
a = [ "dict", "ionary" ]
@this = "is"
}
assign where host.vars.env == "test"
import DirectorOverrideTemplate
}