summaryrefslogtreecommitdiffstats
path: root/application/forms
diff options
context:
space:
mode:
Diffstat (limited to 'application/forms')
-rw-r--r--application/forms/AddToBasketForm.php129
-rw-r--r--application/forms/ApplyMigrationsForm.php54
-rw-r--r--application/forms/BasketCreateSnapshotForm.php37
-rw-r--r--application/forms/BasketForm.php147
-rw-r--r--application/forms/BasketUploadForm.php147
-rw-r--r--application/forms/CustomvarForm.php26
-rw-r--r--application/forms/DeployConfigForm.php121
-rw-r--r--application/forms/DeployFormsBug7530.php126
-rw-r--r--application/forms/DeploymentLinkForm.php170
-rw-r--r--application/forms/DirectorDatafieldCategoryForm.php36
-rw-r--r--application/forms/DirectorDatafieldForm.php301
-rw-r--r--application/forms/DirectorDatalistEntryForm.php80
-rw-r--r--application/forms/DirectorDatalistForm.php45
-rw-r--r--application/forms/DirectorJobForm.php141
-rw-r--r--application/forms/IcingaAddServiceForm.php183
-rw-r--r--application/forms/IcingaAddServiceSetForm.php123
-rw-r--r--application/forms/IcingaApiUserForm.php25
-rw-r--r--application/forms/IcingaCloneObjectForm.php259
-rw-r--r--application/forms/IcingaCommandArgumentForm.php190
-rw-r--r--application/forms/IcingaCommandForm.php137
-rw-r--r--application/forms/IcingaDeleteObjectForm.php41
-rw-r--r--application/forms/IcingaDependencyForm.php309
-rw-r--r--application/forms/IcingaEndpointForm.php61
-rw-r--r--application/forms/IcingaForgetApiKeyForm.php34
-rw-r--r--application/forms/IcingaGenerateApiKeyForm.php42
-rw-r--r--application/forms/IcingaHostForm.php390
-rw-r--r--application/forms/IcingaHostGroupForm.php40
-rw-r--r--application/forms/IcingaHostSelfServiceForm.php156
-rw-r--r--application/forms/IcingaHostVarForm.php36
-rw-r--r--application/forms/IcingaImportObjectForm.php54
-rw-r--r--application/forms/IcingaMultiEditForm.php324
-rw-r--r--application/forms/IcingaNotificationForm.php298
-rw-r--r--application/forms/IcingaObjectFieldForm.php219
-rw-r--r--application/forms/IcingaScheduledDowntimeForm.php133
-rw-r--r--application/forms/IcingaScheduledDowntimeRangeForm.php110
-rw-r--r--application/forms/IcingaServiceDictionaryMemberForm.php54
-rw-r--r--application/forms/IcingaServiceForm.php806
-rw-r--r--application/forms/IcingaServiceGroupForm.php40
-rw-r--r--application/forms/IcingaServiceSetForm.php135
-rw-r--r--application/forms/IcingaServiceVarForm.php36
-rw-r--r--application/forms/IcingaTemplateChoiceForm.php140
-rw-r--r--application/forms/IcingaTimePeriodForm.php82
-rw-r--r--application/forms/IcingaTimePeriodRangeForm.php105
-rw-r--r--application/forms/IcingaUserForm.php214
-rw-r--r--application/forms/IcingaUserGroupForm.php47
-rw-r--r--application/forms/IcingaZoneForm.php43
-rw-r--r--application/forms/ImportCheckForm.php50
-rw-r--r--application/forms/ImportRowModifierForm.php182
-rw-r--r--application/forms/ImportRunForm.php50
-rw-r--r--application/forms/ImportSourceForm.php163
-rw-r--r--application/forms/KickstartForm.php482
-rw-r--r--application/forms/RemoveLinkForm.php59
-rw-r--r--application/forms/RestoreBasketForm.php77
-rw-r--r--application/forms/RestoreObjectForm.php92
-rw-r--r--application/forms/SelfServiceSettingsForm.php306
-rw-r--r--application/forms/SettingsForm.php238
-rw-r--r--application/forms/SyncCheckForm.php69
-rw-r--r--application/forms/SyncPropertyForm.php444
-rw-r--r--application/forms/SyncRuleForm.php112
-rw-r--r--application/forms/SyncRunForm.php67
60 files changed, 8817 insertions, 0 deletions
diff --git a/application/forms/AddToBasketForm.php b/application/forms/AddToBasketForm.php
new file mode 100644
index 0000000..44b5357
--- /dev/null
+++ b/application/forms/AddToBasketForm.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class AddToBasketForm extends DirectorForm
+{
+ /** @var Basket */
+ private $basket;
+
+ private $type = '(has not been set)';
+
+ private $names = [];
+
+ /**
+ * @throws \Zend_Form_Exception
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function setup()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $enum = $db->fetchPairs($db->select()->from('director_basket', [
+ 'a' => 'basket_name',
+ 'b' => 'basket_name',
+ ])->order('basket_name'));
+
+ $names = [];
+ $basket = null;
+ if ($this->hasBeenSent()) {
+ $basketName = $this->getSentValue('basket');
+ if ($basketName) {
+ $basket = Basket::load($basketName, $this->getDb());
+ }
+ }
+ $count = 0;
+ $type = $this->type;
+ foreach ($this->names as $name) {
+ if (! empty($names)) {
+ $names[] = ', ';
+ }
+ if ($basket && $basket->hasObject($type, $name)) {
+ $names[] = Html::tag('span', [
+ 'style' => 'text-decoration: line-through'
+ ], $name);
+ } else {
+ $count++;
+ $names[] = $name;
+ }
+ }
+ $this->addHtmlHint((new HtmlDocument())->add([
+ 'The following objects will be added: ',
+ $names
+ ]));
+ $this->addElement('select', 'basket', [
+ 'label' => $this->translate('Basket'),
+ 'multiOptions' => $this->optionalEnum($enum),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($count > 0) {
+ $this->setSubmitLabel(sprintf(
+ $this->translate('Add %s objects'),
+ $count
+ ));
+ } else {
+ $this->setSubmitLabel($this->translate('Add'));
+ $this->addSubmitButtonIfSet();
+ $this->getElement($this->submitButtonName)->setAttrib('disabled', true);
+ }
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function setNames($names)
+ {
+ $this->names = $names;
+
+ return $this;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $type = $this->type;
+ $basket = Basket::load($this->getValue('basket'), $this->getDb());
+ $basketName = $basket->get('basket_name');
+
+ if (empty($this->names)) {
+ $this->getElement('basket')->addErrorMessage($this->translate(
+ 'No object has been chosen'
+ ));
+ }
+ if ($basket->supportsCustomSelectionFor($type)) {
+ $basket->addObjects($type, $this->names);
+ $basket->store();
+ $this->setSuccessMessage(sprintf($this->translate(
+ 'Configuration objects have been added to the chosen basket "%s"'
+ ), $basketName));
+ return parent::onSuccess();
+ } else {
+ $this->addHtmlHint(Hint::error(Html::sprintf($this->translate(
+ 'Please check your Basket configuration, %s does not support'
+ . ' single "%s" configuration objects'
+ ), Link::create(
+ $basketName,
+ 'director/basket',
+ ['name' => $basketName],
+ ['data-base-target' => '_next']
+ ), $type)));
+
+ return false;
+ }
+ }
+}
diff --git a/application/forms/ApplyMigrationsForm.php b/application/forms/ApplyMigrationsForm.php
new file mode 100644
index 0000000..4f1e62b
--- /dev/null
+++ b/application/forms/ApplyMigrationsForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class ApplyMigrationsForm extends DirectorForm
+{
+ /** @var Migrations */
+ protected $migrations;
+
+ public function setup()
+ {
+ if ($this->migrations->hasSchema()) {
+ $count = $this->migrations->countPendingMigrations();
+ if ($count === 1) {
+ $this->setSubmitLabel(
+ $this->translate('Apply a pending schema migration')
+ );
+ } else {
+ $this->setSubmitLabel(
+ sprintf(
+ $this->translate('Apply %d pending schema migrations'),
+ $count
+ )
+ );
+ }
+ } else {
+ $this->setSubmitLabel($this->translate('Create schema'));
+ }
+ }
+
+ public function onSuccess()
+ {
+ try {
+ $this->setSuccessMessage($this->translate(
+ 'Pending database schema migrations have successfully been applied'
+ ));
+
+ $this->migrations->applyPendingMigrations();
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ }
+ }
+
+ public function setMigrations(Migrations $migrations)
+ {
+ $this->migrations = $migrations;
+ return $this;
+ }
+}
diff --git a/application/forms/BasketCreateSnapshotForm.php b/application/forms/BasketCreateSnapshotForm.php
new file mode 100644
index 0000000..165c7ac
--- /dev/null
+++ b/application/forms/BasketCreateSnapshotForm.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class BasketCreateSnapshotForm extends DirectorForm
+{
+ /** @var Basket */
+ private $basket;
+
+ public function setBasket(Basket $basket)
+ {
+ $this->basket = $basket;
+
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->setSubmitLabel($this->translate('Create Snapshot'));
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ /** @var \Icinga\Module\Director\Db $connection */
+ $connection = $this->basket->getConnection();
+ $snapshot = BasketSnapshot::createForBasket($this->basket, $connection);
+ $snapshot->store();
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/BasketForm.php b/application/forms/BasketForm.php
new file mode 100644
index 0000000..8ff6cca
--- /dev/null
+++ b/application/forms/BasketForm.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Zend_Form_SubForm as ZfSubForm;
+
+class BasketForm extends DirectorObjectForm
+{
+ protected $listUrl = 'director/baskets';
+
+ protected function getAvailableTypes()
+ {
+ return [
+ 'Command' => $this->translate('Command Definitions'),
+ 'ExternalCommand' => $this->translate('External Command Definitions'),
+ 'CommandTemplate' => $this->translate('Command Template'),
+ 'HostGroup' => $this->translate('Host Group'),
+ 'IcingaTemplateChoiceHost' => $this->translate('Host Template Choice'),
+ 'HostTemplate' => $this->translate('Host Templates'),
+ 'ServiceGroup' => $this->translate('Service Groups'),
+ 'IcingaTemplateChoiceService' => $this->translate('Service Template Choice'),
+ 'ServiceTemplate' => $this->translate('Service Templates'),
+ 'ServiceSet' => $this->translate('Service Sets'),
+ 'UserGroup' => $this->translate('User Groups'),
+ 'UserTemplate' => $this->translate('User Templates'),
+ 'User' => $this->translate('Users'),
+ 'NotificationTemplate' => $this->translate('Notification Templates'),
+ 'Notification' => $this->translate('Notifications'),
+ 'TimePeriod' => $this->translate('Time Periods'),
+ 'Dependency' => $this->translate('Dependencies'),
+ 'DataList' => $this->translate('Data Lists'),
+ 'ImportSource' => $this->translate('Import Sources'),
+ 'SyncRule' => $this->translate('Sync Rules'),
+ 'DirectorJob' => $this->translate('Job Definitions'),
+ 'Basket' => $this->translate('Basket Definitions'),
+ ];
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'basket_name', [
+ 'label' => $this->translate('Basket Name'),
+ 'required' => true,
+ ]);
+
+ $types = $this->getAvailableTypes();
+
+ $options = [
+ 'IGNORE' => $this->translate('Ignore'),
+ 'ALL' => $this->translate('All of them'),
+ '[]' => $this->translate('Custom Selection'),
+ ];
+
+ $this->addHtmlHint($this->translate(
+ 'What should we place into this Basket every time we create'
+ . ' new snapshot?'
+ ));
+
+ $sub = new ZfSubForm();
+ $sub->setDecorators([
+ ['HtmlTag', ['tag' => 'dl']],
+ 'FormElements'
+ ]);
+
+ foreach ($types as $name => $label) {
+ $sub->addElement('select', $name, [
+ 'label' => $label,
+ 'multiOptions' => $options,
+ ]);
+ }
+
+ $this->addSubForm($sub, 'objects');
+ $this->addDeleteButton();
+
+ $this->addHtmlHint($this->translate(
+ 'Choose "All" to always add all of them,'
+ . ' "Ignore" to not care about a specific Type at all and'
+ . ' opt for "Custom Selection" in case you want to choose'
+ . ' just some specific Objects.'
+ ));
+ }
+
+ protected function setDefaultsFromObject(DbObject $object)
+ {
+ parent::setDefaultsFromObject($object);
+ /** @var Basket $object */
+ $values = [];
+ foreach ($this->getAvailableTypes() as $type => $label) {
+ $values[$type] = 'IGNORE';
+ }
+ foreach ($object->getChosenObjects() as $type => $selection) {
+ if ($selection === true) {
+ $values[$type] = 'ALL';
+ } elseif (is_array($selection)) {
+ $values[$type] = '[]';
+ }
+ }
+
+ $this->populate([
+ 'objects' => $values
+ ]);
+ }
+
+ protected function onRequest()
+ {
+ parent::onRequest(); // TODO: Change the autogenerated stub
+ }
+
+ protected function getObjectClassname()
+ {
+ return Basket::class;
+ }
+
+ public function onSuccess()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+
+ if ($basket->isEmpty()) {
+ $this->addError($this->translate("It's not allowed to store an empty basket"));
+
+ return;
+ }
+ if (! $basket->hasBeenLoadedFromDb()) {
+ $basket->set('owner_type', 'user');
+ $basket->set('owner_value', $this->getAuth()->getUser()->getUsername());
+ }
+
+ parent::onSuccess();
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+ $this->setSuccessUrl(
+ 'director/basket',
+ ['name' => $basket->get('basket_name')]
+ );
+ }
+}
diff --git a/application/forms/BasketUploadForm.php b/application/forms/BasketUploadForm.php
new file mode 100644
index 0000000..a88dc06
--- /dev/null
+++ b/application/forms/BasketUploadForm.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Notification;
+
+class BasketUploadForm extends DirectorObjectForm
+{
+ protected $listUrl = 'director/baskets';
+
+ protected $failed;
+
+ protected $upload;
+
+ protected $rawUpload;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'basket_name', [
+ 'label' => $this->translate('Basket Name'),
+ 'required' => true,
+ ]);
+ $this->setAttrib('enctype', 'multipart/form-data');
+
+ $this->addElement('file', 'uploaded_file', [
+ 'label' => $this->translate('Choose file'),
+ 'destination' => $this->getTempDir(),
+ 'valueDisabled' => true,
+ 'isArray' => false,
+ 'multiple' => false,
+ 'ignore' => true,
+ ]);
+
+ $this->setSubmitLabel($this->translate('Upload'));
+ }
+
+ protected function getTempDir()
+ {
+ return sys_get_temp_dir();
+ }
+
+ protected function getObjectClassname()
+ {
+ return '\\Icinga\\Module\\Director\\DirectorObject\\Automation\\Basket';
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+ $this->setSuccessUrl(
+ 'director/basket',
+ ['name' => $basket->get('basket_name')]
+ );
+ }
+
+ /**
+ * @return bool
+ * @throws IcingaException
+ */
+ protected function processUploadedSource()
+ {
+ if (! array_key_exists('uploaded_file', $_FILES)) {
+ throw new IcingaException('Got no file');
+ }
+
+ if (! isset($_FILES['uploaded_file']['tmp_name'])
+ || ! is_uploaded_file($_FILES['uploaded_file']['tmp_name'])
+ ) {
+ $this->addError('Got no uploaded file');
+ $this->failed = true;
+
+ return false;
+ }
+ $tmpFile = $_FILES['uploaded_file']['tmp_name'];
+ $originalFilename = $_FILES['uploaded_file']['name'];
+
+ $source = file_get_contents($tmpFile);
+ unlink($tmpFile);
+ try {
+ $json = Json::decode($source);
+ $this->rawUpload = $source;
+ $this->upload = $json;
+ } catch (Exception $e) {
+ $this->addError($originalFilename . ' failed: ' . $e->getMessage());
+ Notification::error($originalFilename . ' failed: ' . $e->getMessage());
+ $this->failed = true;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function onRequest()
+ {
+ if ($this->hasBeenSent()) {
+ try {
+ $this->processUploadedSource();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ return;
+ }
+ }
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ /** @var Basket $basket */
+ $basket = $this->object();
+
+ foreach ($this->upload as $type => $content) {
+ if ($type !== 'Datafield') {
+ $basket->addObjects($type, array_keys((array) $content));
+ }
+ }
+ if ($basket->isEmpty()) {
+ $this->addError($this->translate("It's not allowed to store an empty basket"));
+
+ return;
+ }
+
+ $basket->set('owner_type', 'user');
+ $basket->set('owner_value', $this->getAuth()->getUser()->getUsername());
+ $basket->store($this->db);
+
+ BasketSnapshot::forBasketFromJson(
+ $basket,
+ $this->rawUpload
+ )->store($this->db);
+ $this->setObjectSuccessUrl();
+ $this->beforeSuccessfulRedirect();
+ $this->redirectOnSuccess($this->translate('Basket has been uploaded'));
+ }
+}
diff --git a/application/forms/CustomvarForm.php b/application/forms/CustomvarForm.php
new file mode 100644
index 0000000..759464c
--- /dev/null
+++ b/application/forms/CustomvarForm.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class CustomvarForm extends QuickForm
+{
+ protected $submitLabel = false;
+
+ public function setup()
+ {
+ $this->removeCsrfToken();
+ $this->removeElement(self::ID);
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Variable name'),
+ 'required' => true,
+ ));
+
+ $this->addElement('text', 'varvalue', array(
+ 'label' => $this->translate('Value'),
+ ));
+
+ // $this->addHidden('format', 'string'); // expression, json?
+ }
+}
diff --git a/application/forms/DeployConfigForm.php b/application/forms/DeployConfigForm.php
new file mode 100644
index 0000000..0b817fa
--- /dev/null
+++ b/application/forms/DeployConfigForm.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\DeploymentApiInterface;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class DeployConfigForm extends DirectorForm
+{
+ use DeployFormsBug7530;
+
+ /** @var DeploymentApiInterface */
+ private $api;
+
+ /** @var string */
+ private $checksum;
+
+ /** @var int */
+ private $deploymentId;
+
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ public function setup()
+ {
+ $activities = $this->db->countActivitiesSinceLastDeployedConfig();
+ if ($this->deploymentId) {
+ $label = $this->translate('Re-deploy now');
+ } elseif ($activities === 0) {
+ $label = $this->translate('There are no pending changes. Deploy anyway');
+ } else {
+ $label = sprintf(
+ $this->translate('Deploy %d pending changes'),
+ $activities
+ );
+ }
+
+ if ($this->deploymentId) {
+ $deployIcon = 'reply-all';
+ } else {
+ $deployIcon = 'forward';
+ }
+
+ $this->addHtml(
+ $this->getView()->icon(
+ $deployIcon,
+ $label,
+ array('class' => 'link-color')
+ ) . '<nobr>'
+ );
+
+ $el = $this->createElement('submit', 'btn_deploy', array(
+ 'label' => $label,
+ 'escape' => false,
+ 'decorators' => array('ViewHelper'),
+ 'class' => 'link-button ' . $deployIcon,
+ ));
+
+ $this->addHtml('</nobr>');
+ $this->submitButtonName = $el->getName();
+ $this->setSubmitLabel($label);
+ $this->addElement($el);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->skipBecauseOfBug7530()) {
+ return;
+ }
+
+ $db = $this->db;
+ $msg = $this->translate('Config has been submitted, validation is going on');
+ $this->setSuccessMessage($msg);
+
+ $isApiRequest = $this->getRequest()->isApiRequest();
+ if ($this->checksum) {
+ $config = IcingaConfig::load(hex2bin($this->checksum), $db);
+ } else {
+ $config = IcingaConfig::generate($db);
+ }
+
+ $this->api->wipeInactiveStages($db);
+
+ if ($this->api->dumpConfig($config, $db)) {
+ if ($isApiRequest) {
+ die('Api not ready');
+ // return $this->sendJson((object) array('checksum' => $checksum));
+ } else {
+ $this->setSuccessUrl('director/config/deployments');
+ $this->setSuccessMessage(
+ $this->translate('Config has been submitted, validation is going on')
+ );
+ }
+ parent::onSuccess();
+ } else {
+ throw new IcingaException($this->translate('Config deployment failed'));
+ }
+ }
+
+ public function setChecksum($checksum)
+ {
+ $this->checksum = $checksum;
+ return $this;
+ }
+
+ public function setDeploymentId($id)
+ {
+ $this->deploymentId = $id;
+ return $this;
+ }
+
+ public function setApi(DeploymentApiInterface $api)
+ {
+ $this->api = $api;
+ return $this;
+ }
+}
diff --git a/application/forms/DeployFormsBug7530.php b/application/forms/DeployFormsBug7530.php
new file mode 100644
index 0000000..4d456ae
--- /dev/null
+++ b/application/forms/DeployFormsBug7530.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Module\Director\Core\CoreApi;
+use ipl\Html\Html;
+
+trait DeployFormsBug7530
+{
+ public function hasBeenSubmitted()
+ {
+ if (parent::hasBeenSubmitted()) {
+ return true;
+ } else {
+ return \strlen($this->getSentValue('confirm_7530')) > 0;
+ }
+ }
+
+ protected function shouldWarnAboutBug7530()
+ {
+ /** @var \Icinga\Module\Director\Db $db */
+ $db = $this->getDb();
+
+ return $db->settings()->get('ignore_bug7530') !== 'y'
+ && $this->getSentValue('confirm_7530') !== 'i_know'
+ && $this->configMightTriggerBug7530()
+ & $this->coreHasBug7530();
+ }
+
+ protected function configMightTriggerBug7530()
+ {
+ /** @var \Icinga\Module\Director\Db $connection */
+ $connection = $this->getDb();
+ $db = $connection->getDbAdapter();
+
+ $zoneIds = $db->fetchCol(
+ $db->select()
+ ->from('icinga_zone', 'id')
+ ->where('object_type = ?', 'object')
+ );
+ if (empty($zoneIds)) {
+ return false;
+ }
+
+ $objectTypes = [
+ 'icinga_host',
+ 'icinga_service',
+ 'icinga_notification',
+ 'icinga_command',
+ ];
+
+ foreach ($objectTypes as $objectType) {
+ if ((int) $db->fetchOne(
+ $db->select()
+ ->from($objectType, 'COUNT(*)')
+ ->where('zone_id IN (?)', $zoneIds)
+ ) > 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function coreHasBug7530()
+ {
+ // TODO: Cache this
+ if ($this->api instanceof CoreApi) {
+ $version = $this->api->getVersion();
+ if ($version === null) {
+ throw new \RuntimeException($this->translate('Unable to detect your Icinga 2 Core version'));
+ } elseif (\version_compare($version, '2.11.0', '>=')
+ && \version_compare($version, '2.12.0', '<')
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function skipBecauseOfBug7530()
+ {
+ $bug7530 = $this->getSentValue('confirm_7530');
+ if ($bug7530 === 'whaaat') {
+ $this->setSuccessMessage($this->translate('Config has not been deployed'));
+ parent::onSuccess();
+ } elseif ($bug7530 === 'hell_yes') {
+ $this->db->settings()->set('ignore_bug7530', 'y');
+ }
+ if ($this->shouldWarnAboutBug7530()) {
+ $this->addHtml(Hint::warning(Html::sprintf($this->translate(
+ "Warning: you're running Icinga v2.11.0 and our configuration looks"
+ . " like you could face issue %s. We're already working on a solution."
+ . " The GitHub Issue and our %s contain related details."
+ ), Html::tag('a', [
+ 'href' => 'https://github.com/Icinga/icinga2/issues/7530',
+ 'target' => '_blank',
+ 'title' => sprintf(
+ $this->translate('Show Issue %s on GitHub'),
+ '7530'
+ ),
+ 'class' => 'icon-github-circled',
+ ], '#7530'), Html::tag('a', [
+ 'href' => 'https://icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/'
+ . '#config-sync-zones-in-zones',
+ 'target' => '_blank',
+ 'title' => $this->translate('Upgrading Icinga 2 - Confic Sync: Zones in Zones'),
+ 'class' => 'icon-info-circled',
+ ], $this->translate('Upgrading documentation')))));
+ $this->addElement('select', 'confirm_7530', [
+ 'multiOptions' => $this->optionalEnum([
+ 'i_know' => $this->translate("I know what I'm doing, deploy anyway"),
+ 'hell_yes' => $this->translate("I know, please don't bother me again"),
+ 'whaaat' => $this->translate("Thanks, I'll verify this and come back later"),
+ ]),
+ 'class' => 'autosubmit',
+ 'decorators' => ['ViewHelper'],
+ ]);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/application/forms/DeploymentLinkForm.php b/application/forms/DeploymentLinkForm.php
new file mode 100644
index 0000000..f42a627
--- /dev/null
+++ b/application/forms/DeploymentLinkForm.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Authentication\Auth;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Core\DeploymentApiInterface;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Deployment\DeploymentInfo;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use gipfl\IcingaWeb2\Icon;
+use Zend_View_Interface;
+
+class DeploymentLinkForm extends DirectorForm
+{
+ use DeployFormsBug7530;
+
+ /** @var DeploymentInfo */
+ protected $info;
+
+ /** @var Auth */
+ protected $auth;
+
+ /** @var DeploymentApiInterface */
+ protected $api;
+
+ /** @var Db */
+ protected $db;
+
+ /**
+ * @param DeploymentInfo $info
+ * @param Auth $auth
+ * @return static
+ */
+ public static function create(Db $db, DeploymentInfo $info, Auth $auth, DeploymentApiInterface $api)
+ {
+ $self = static::load();
+ $self->setAuth($auth);
+ $self->db = $db;
+ $self->info = $info;
+ $self->api = $api;
+ return $self;
+ }
+
+ public function setAuth(Auth $auth)
+ {
+ $this->auth = $auth;
+ return $this;
+ }
+
+ public function setup()
+ {
+ if (! $this->canDeploy()) {
+ return;
+ }
+
+ $onObject = $this->info->getSingleObjectChanges();
+ $total = $this->info->getTotalChanges();
+
+ if ($onObject === 0) {
+ if ($total === 1) {
+ $msg = $this->translate('There is a single pending change');
+ } else {
+ $msg = sprintf(
+ $this->translate('There are %d pending changes'),
+ $total
+ );
+ }
+ } elseif ($total === 1) {
+ $msg = $this->translate('There has been a single change to this object, nothing else has been modified');
+ } elseif ($total === $onObject) {
+ $msg = sprintf(
+ $this->translate('There have been %d changes to this object, nothing else has been modified'),
+ $onObject
+ );
+ } else {
+ $msg = sprintf(
+ $this->translate('There are %d pending changes, %d of them applied to this object'),
+ $total,
+ $onObject
+ );
+ }
+
+ $this->setAttrib('class', 'gipfl-inline-form');
+ $this->addHtml(Icon::create('wrench'));
+ try {
+ // As this is shown for single objects, ignore errors caused by an
+ // unreachable core
+ $target = $this->shouldWarnAboutBug7530() ? '_self' : '_next';
+ } catch (\Exception $e) {
+ $target = '_next';
+ }
+ $this->addSubmitButton($this->translate('Deploy'), [
+ 'class' => 'link-button icon-wrench',
+ 'title' => $msg,
+ 'data-base-target' => $target,
+ ]);
+ }
+
+ protected function canDeploy()
+ {
+ return $this->auth->hasPermission('director/deploy');
+ }
+
+ public function render(Zend_View_Interface $view = null)
+ {
+ if (! $this->canDeploy()) {
+ return '';
+ }
+
+ return parent::render($view);
+ }
+
+ public function onSuccess()
+ {
+ try {
+ if ($this->skipBecauseOfBug7530()) {
+ return;
+ }
+ } catch (\Exception $e) {
+ // continue
+ }
+ $this->deploy();
+ }
+
+ public function deploy()
+ {
+ $this->setSuccessUrl('director/config/deployments');
+ $config = IcingaConfig::generate($this->db);
+ $checksum = $config->getHexChecksum();
+
+ try {
+ $this->api->wipeInactiveStages($this->db);
+ } catch (\Exception $e) {
+ $this->notifyError($e->getMessage());
+ }
+
+ if ($this->api->dumpConfig($config, $this->db)) {
+ $this->deploymentSucceeded($checksum);
+ } else {
+ $this->deploymentFailed($checksum);
+ }
+ }
+
+ protected function deploymentSucceeded($checksum)
+ {
+ if ($this->getRequest()->isApiRequest()) {
+ throw new IcingaException('Not yet');
+ // $this->sendJson($this->getResponse(), (object) array('checksum' => $checksum));
+ } else {
+ $msg = $this->translate('Config has been submitted, validation is going on');
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ protected function deploymentFailed($checksum, $error = null)
+ {
+ $extra = $error ? ': ' . $error: '';
+
+ if ($this->getRequest()->isApiRequest()) {
+ throw new IcingaException('Not yet');
+ // $this->sendJsonError($this->getResponse(), 'Config deployment failed' . $extra);
+ } else {
+ $msg = $this->translate('Config deployment failed') . $extra;
+ $this->notifyError($msg);
+ $this->redirectAndExit('director/config/deployments');
+ }
+ }
+}
diff --git a/application/forms/DirectorDatafieldCategoryForm.php b/application/forms/DirectorDatafieldCategoryForm.php
new file mode 100644
index 0000000..fe5efc9
--- /dev/null
+++ b/application/forms/DirectorDatafieldCategoryForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class DirectorDatafieldCategoryForm extends DirectorObjectForm
+{
+ protected $objectName = 'Data field category';
+
+ protected $listUrl = 'director/data/fieldcategories';
+
+ public function setup()
+ {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Data field categories allow to structure Data Fields. Fields with'
+ . ' a category will be shown grouped by category.'
+ )
+ );
+
+ $this->addElement('text', 'category_name', [
+ 'label' => $this->translate('Category name'),
+ 'description' => $this->translate(
+ 'The unique name of the category used for grouping your custom Data Fields.'
+ ),
+ 'required' => true,
+ ]);
+
+ $this->addElement('text', 'description', [
+ 'label' => $this->translate('Description'),
+ ]);
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/DirectorDatafieldForm.php b/application/forms/DirectorDatafieldForm.php
new file mode 100644
index 0000000..a306bd7
--- /dev/null
+++ b/application/forms/DirectorDatafieldForm.php
@@ -0,0 +1,301 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\CustomVariable\CustomVariables;
+use Icinga\Module\Director\Hook\DataTypeHook;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Application\Hook;
+use Exception;
+
+class DirectorDatafieldForm extends DirectorObjectForm
+{
+ protected $objectName = 'Data field';
+
+ protected $listUrl = 'director/data/fields';
+
+ protected function onRequest()
+ {
+ if ($this->hasBeenSent()) {
+ if ($this->shouldBeDeleted()) {
+ $varname = $this->getSentValue('varname');
+ if ($cnt = CustomVariables::countAll($varname, $this->getDb())) {
+ $this->askForVariableDeletion($varname, $cnt);
+ }
+ } elseif ($this->shouldBeRenamed()) {
+ $varname = $this->object()->getOriginalProperty('varname');
+ if ($cnt = CustomVariables::countAll($varname, $this->getDb())) {
+ $this->askForVariableRename(
+ $varname,
+ $this->getSentValue('varname'),
+ $cnt
+ );
+ }
+ }
+ }
+
+ parent::onRequest();
+ }
+
+ protected function askForVariableDeletion($varname, $cnt)
+ {
+ $msg = $this->translate(
+ 'Leaving custom variables in place while removing the related field is'
+ . ' perfectly legal and might be a desired operation. This way you can'
+ . ' no longer modify related custom variables in the Director GUI, but'
+ . ' the variables themselves will stay there and continue to be deployed.'
+ . ' When you re-add a field for the same variable later on, everything'
+ . ' will continue to work as before'
+ );
+
+ $this->addBoolean('wipe_vars', array(
+ 'label' => $this->translate('Wipe related vars'),
+ 'description' => sprintf($msg, $this->getSentValue('varname')),
+ 'required' => true,
+ ));
+
+ if ($wipe = $this->getSentValue('wipe_vars')) {
+ if ($wipe === 'y') {
+ CustomVariables::deleteAll($varname, $this->getDb());
+ }
+ } else {
+ $this->abortDeletion();
+ $this->addError(
+ sprintf(
+ $this->translate('Also wipe all "%s" custom variables from %d objects?'),
+ $varname,
+ $cnt
+ )
+ );
+ $this->getElement('wipe_vars')->addError(
+ sprintf(
+ $this->translate(
+ 'There are %d objects with a related property. Should I also'
+ . ' remove the "%s" property from them?'
+ ),
+ $cnt,
+ $varname
+ )
+ );
+ }
+ }
+
+ protected function askForVariableRename($oldname, $newname, $cnt)
+ {
+ $msg = $this->translate(
+ 'Leaving custom variables in place while renaming the related field is'
+ . ' perfectly legal and might be a desired operation. This way you can'
+ . ' no longer modify related custom variables in the Director GUI, but'
+ . ' the variables themselves will stay there and continue to be deployed.'
+ . ' When you re-add a field for the same variable later on, everything'
+ . ' will continue to work as before'
+ );
+
+ $this->addBoolean('rename_vars', array(
+ 'label' => $this->translate('Rename related vars'),
+ 'description' => sprintf($msg, $this->getSentValue('varname')),
+ 'required' => true,
+ ));
+
+ if ($wipe = $this->getSentValue('rename_vars')) {
+ if ($wipe === 'y') {
+ CustomVariables::renameAll($oldname, $newname, $this->getDb());
+ }
+ } else {
+ $this->abortDeletion();
+ $this->addError(
+ sprintf(
+ $this->translate('Also rename all "%s" custom variables to "%s" on %d objects?'),
+ $oldname,
+ $newname,
+ $cnt
+ )
+ );
+ $this->getElement('rename_vars')->addError(
+ sprintf(
+ $this->translate(
+ 'There are %d objects with a related property. Should I also'
+ . ' rename the "%s" property to "%s" on them?'
+ ),
+ $cnt,
+ $oldname,
+ $newname
+ )
+ );
+ }
+ }
+
+ public function setup()
+ {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Data fields allow you to customize input controls for Icinga custom'
+ . ' variables. Once you defined them here, you can provide them through'
+ . ' your defined templates. This gives you a granular control over what'
+ . ' properties your users should be allowed to configure in which way.'
+ )
+ );
+
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Field name'),
+ 'description' => $this->translate(
+ 'The unique name of the field. This will be the name of the custom'
+ . ' variable in the rendered Icinga configuration.'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('text', 'caption', array(
+ 'label' => $this->translate('Caption'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'The caption which should be displayed to your users when this field'
+ . ' is shown'
+ )
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this field. Will be shown as soon as a'
+ . ' user puts the focus on this field'
+ ),
+ 'rows' => '3',
+ ));
+
+ $this->addElement('select', 'category_id', [
+ 'label' => $this->translate('Data Field Category'),
+ 'multiOptions' => $this->optionalEnum($this->enumCategpories()),
+ ]);
+
+ $error = false;
+ try {
+ $types = $this->enumDataTypes();
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ $types = $this->optionalEnum(array());
+ }
+
+ $this->addElement('select', 'datatype', array(
+ 'label' => $this->translate('Data type'),
+ 'description' => $this->translate('Field type'),
+ 'required' => true,
+ 'multiOptions' => $types,
+ 'class' => 'autosubmit',
+ ));
+ if ($error) {
+ $this->getElement('datatype')->addError($error);
+ }
+
+ $object = $this->object();
+ try {
+ if ($class = $this->getSentValue('datatype')) {
+ if ($class && array_key_exists($class, $types)) {
+ $this->addSettings($class);
+ }
+ } elseif ($class = $object->get('datatype')) {
+ $this->addSettings($class);
+ }
+
+ // TODO: next line looks like obsolete duplicate code to me
+ $this->addSettings();
+ } catch (Exception $e) {
+ $this->getElement('datatype')->addError($e->getMessage());
+ }
+
+ foreach ($object->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+
+ $this->setButtons();
+ }
+
+ public function shouldBeRenamed()
+ {
+ $object = $this->object();
+ return $object->hasBeenLoadedFromDb()
+ && $object->getOriginalProperty('varname') !== $this->getSentValue('varname');
+ }
+
+ protected function addSettings($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getValue('datatype');
+ }
+
+ if ($class !== null) {
+ if (! class_exists($class)) {
+ throw new ConfigurationError(
+ 'The hooked class "%s" for this data field does no longer exist',
+ $class
+ );
+ }
+
+ $class::addSettingsFormFields($this);
+ }
+ }
+
+ protected function clearOutdatedSettings()
+ {
+ $names = array();
+ $object = $this->object();
+ $global = array('varname', 'description', 'caption', 'datatype');
+
+ /** @var \Zend_Form_Element $el */
+ foreach ($this->getElements() as $el) {
+ if ($el->getIgnore()) {
+ continue;
+ }
+
+ $name = $el->getName();
+ if (in_array($name, $global)) {
+ continue;
+ }
+
+ $names[$name] = $name;
+ }
+
+
+ foreach ($object->getSettings() as $setting => $value) {
+ if (! array_key_exists($setting, $names)) {
+ unset($object->$setting);
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ $this->clearOutdatedSettings();
+
+ if ($class = $this->getValue('datatype')) {
+ if (array_key_exists($class, $this->enumDataTypes())) {
+ $this->addHidden('format', $class::getFormat());
+ }
+ }
+
+ parent::onSuccess();
+ }
+
+ protected function enumDataTypes()
+ {
+ $hooks = Hook::all('Director\\DataType');
+ $enum = array(null => '- please choose -');
+ /** @var DataTypeHook $hook */
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+
+ return $enum;
+ }
+
+ protected function enumCategpories()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ return $db->fetchPairs(
+ $db->select()->from('director_datafield_category', ['id', 'category_name'])
+ );
+ }
+}
diff --git a/application/forms/DirectorDatalistEntryForm.php b/application/forms/DirectorDatalistEntryForm.php
new file mode 100644
index 0000000..c6e309f
--- /dev/null
+++ b/application/forms/DirectorDatalistEntryForm.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class DirectorDatalistEntryForm extends DirectorObjectForm
+{
+ /** @var DirectorDatalist */
+ protected $datalist;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'entry_name', [
+ 'label' => $this->translate('Key'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Will be stored as a custom variable value when this entry'
+ . ' is chosen from the list'
+ )
+ ]);
+
+ $this->addElement('text', 'entry_value', [
+ 'label' => $this->translate('Label'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This will be the visible caption for this entry'
+ )
+ ]);
+
+ $rolesConfig = Config::app('roles', true);
+ $roles = [];
+ foreach ($rolesConfig as $name => $role) {
+ $roles[$name] = $name;
+ }
+
+ $this->addElement('extensibleSet', 'allowed_roles', [
+ 'label' => $this->translate('Allowed roles'),
+ 'required' => false,
+ 'multiOptions' => $roles,
+ 'description' => $this->translate(
+ 'Allow to use this entry only to users with one of these Icinga Web 2 roles'
+ )
+ ]);
+
+ $this->addHidden('list_id', $this->datalist->get('id'));
+ $this->addHidden('format', 'string');
+ if (!$this->isNew()) {
+ $this->addHidden('entry_name', $this->object->get('entry_name'));
+ }
+
+ $this->addSimpleDisplayGroup(['entry_name', 'entry_value', 'allowed_roles'], 'entry', [
+ 'legend' => $this->isNew()
+ ? $this->translate('Add data list entry')
+ : $this->translate('Modify data list entry')
+ ]);
+
+ $this->setButtons();
+ }
+
+ /**
+ * @param DirectorDatalist $list
+ * @return $this
+ */
+ public function setList(DirectorDatalist $list)
+ {
+ $this->datalist = $list;
+ /** @var Db $db */
+ $db = $list->getConnection();
+ $this->setDb($db);
+
+ return $this;
+ }
+}
diff --git a/application/forms/DirectorDatalistForm.php b/application/forms/DirectorDatalistForm.php
new file mode 100644
index 0000000..91c0ea7
--- /dev/null
+++ b/application/forms/DirectorDatalistForm.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Authentication\Auth;
+
+class DirectorDatalistForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('text', 'list_name', array(
+ 'label' => $this->translate('List name'),
+ 'description' => $this->translate(
+ 'Data lists are mainly used as data providers for custom variables'
+ . ' presented as dropdown boxes boxes. You can manually manage'
+ . ' their entries here in place, but you could also create dedicated'
+ . ' sync rules after creating a new empty list. This would allow you'
+ . ' to keep your available choices in sync with external data providers'
+ ),
+ 'required' => true,
+ ));
+ $this->addSimpleDisplayGroup(array('list_name'), 'list', array(
+ 'legend' => $this->translate('Data list')
+ ));
+
+ $this->setButtons();
+ }
+
+ public function onSuccess()
+ {
+ $this->object()->set('owner', self::username());
+ parent::onSuccess();
+ }
+
+ protected static function username()
+ {
+ $auth = Auth::getInstance();
+ if ($auth->isAuthenticated()) {
+ return $auth->getUser()->getUsername();
+ } else {
+ return '<unknown>';
+ }
+ }
+}
diff --git a/application/forms/DirectorJobForm.php b/application/forms/DirectorJobForm.php
new file mode 100644
index 0000000..7ca998c
--- /dev/null
+++ b/application/forms/DirectorJobForm.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
+use Icinga\Module\Director\Hook\JobHook;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Hook;
+
+class DirectorJobForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $jobTypes = $this->enumJobTypes();
+
+ $this->addElement('select', 'job_class', array(
+ 'label' => $this->translate('Job Type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($jobTypes),
+ 'description' => $this->translate(
+ 'These are different available job types'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ if (! $jobClass = $this->getJobClass()) {
+ return;
+ }
+
+ if ($desc = $jobClass::getDescription($this)) {
+ $this->addHtmlHint($desc);
+ }
+
+ $this->addBoolean(
+ 'disabled',
+ array(
+ 'label' => $this->translate('Disabled'),
+ 'description' => $this->translate(
+ 'This allows to temporarily disable this job'
+ )
+ ),
+ 'n'
+ );
+
+ $this->addElement('text', 'run_interval', array(
+ 'label' => $this->translate('Run interval'),
+ 'description' => $this->translate(
+ 'Execution interval for this job, in seconds'
+ ),
+ 'value' => $jobClass::getSuggestedRunInterval($this)
+ ));
+
+ $periods = $this->db->enumTimeperiods();
+
+ if (!empty($periods)) {
+ $this->addElement(
+ 'select',
+ 'timeperiod_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period within this job should be active.'
+ . ' Supports only simple time periods (weekday and multiple'
+ . ' time definitions)'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+ }
+
+ $this->addElement('text', 'job_name', array(
+ 'label' => $this->translate('Job name'),
+ 'description' => $this->translate(
+ 'A short name identifying this job. Use something meaningful,'
+ . ' like "Import Puppet Hosts"'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addSettings();
+ $this->setButtons();
+ }
+
+ public function getSentOrObjectSetting($name, $default = null)
+ {
+ if ($this->hasObject()) {
+ $value = $this->getSentValue($name);
+ if ($value === null) {
+ /** @var DbObjectWithSettings $object */
+ $object = $this->getObject();
+ return $object->getSetting($name, $default);
+ } else {
+ return $value;
+ }
+ } else {
+ return $this->getSentValue($name, $default);
+ }
+ }
+
+ protected function getJobClass($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getSentOrObjectValue('job_class');
+ }
+
+ if (array_key_exists($class, $this->enumJobTypes())) {
+ return $class;
+ }
+
+ return null;
+ }
+
+ protected function addSettings($class = null)
+ {
+ if (! $class = $this->getJobClass($class)) {
+ return;
+ }
+
+ $class::addSettingsFormFields($this);
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+ }
+
+ protected function enumJobTypes()
+ {
+ /** @var JobHook[] $hooks */
+ $hooks = Hook::all('Director\\Job');
+
+ $enum = array();
+
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+ asort($enum);
+
+ return $enum;
+ }
+}
diff --git a/application/forms/IcingaAddServiceForm.php b/application/forms/IcingaAddServiceForm.php
new file mode 100644
index 0000000..df2302e
--- /dev/null
+++ b/application/forms/IcingaAddServiceForm.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\IcingaWeb2\Link;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaService;
+
+class IcingaAddServiceForm extends DirectorObjectForm
+{
+ /** @var IcingaHost[] */
+ private $hosts;
+
+ /** @var IcingaHost */
+ private $host;
+
+ /** @var IcingaService */
+ protected $object;
+
+ protected $objectName = 'service';
+
+ public function setup()
+ {
+ if ($this->object === null) {
+ $this->object = IcingaService::create(
+ ['object_type' => 'object'],
+ $this->db
+ );
+ }
+
+ $this->addSingleImportElement();
+
+ if (! ($imports = $this->getSentOrObjectValue('imports'))) {
+ $this->setSubmitLabel($this->translate('Next'));
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->removeElement('imports');
+ $this->addHidden('imports', $imports);
+ $this->setElementValue('imports', $imports);
+ $this->addNameElement();
+ $name = $this->getSentOrObjectValue('object_name');
+ if (empty($name)) {
+ $this->setElementValue('object_name', $imports);
+ }
+ $this->groupMainProperties()
+ ->setButtons();
+ }
+
+ protected function groupMainProperties($importsFirst = false)
+ {
+ $elements = [
+ 'object_type',
+ 'imports',
+ 'object_name',
+ ];
+
+ $this->addDisplayGroup($elements, 'object_definition', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
+ 'legend' => $this->translate('Main properties')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @param bool $required
+ * @return $this
+ */
+ protected function addSingleImportElement($required = null)
+ {
+ $enum = $this->enumServiceTemplates();
+ if (empty($enum)) {
+ if ($required) {
+ if ($this->hasBeenSent()) {
+ $this->addError($this->translate('No service has been chosen'));
+ } else {
+ if ($this->hasPermission('director/admin')) {
+ $html = sprintf(
+ $this->translate('Please define a %s first'),
+ Link::create(
+ $this->translate('Service Template'),
+ 'director/service/add',
+ ['type' => 'template']
+ )
+ );
+ } else {
+ $html = $this->translate('No Service Templates have been provided yet');
+ }
+ $this->addHtml('<p class="warning">' . $html . '</p>');
+ }
+ }
+
+ return $this;
+ }
+ $this->addElement('text', 'imports', [
+ 'label' => $this->translate('Service'),
+ 'description' => $this->translate('Choose a service template'),
+ 'required' => true,
+ 'data-suggestion-context' => 'servicetemplates',
+ 'class' => 'autosubmit director-suggest'
+ ]);
+
+ return $this;
+ }
+
+ protected function enumServiceTemplates()
+ {
+ $tpl = $this->getDb()->enumIcingaTemplates('service');
+ return array_combine($tpl, $tpl);
+ }
+
+ /**
+ * @param IcingaHost[] $hosts
+ * @return $this
+ */
+ public function setHosts(array $hosts)
+ {
+ $this->hosts = $hosts;
+ return $this;
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ protected function addNameElement()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Name for the Icinga service you are going to create'
+ )
+ ]);
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ if ($this->host !== null) {
+ if ($id = $this->host->get('id')) {
+ $this->object->set('host_id', $id);
+ } else {
+ $this->object->set('host', $this->host->getObjectName());
+ }
+ parent::onSuccess();
+ return;
+ }
+
+ $plain = $this->object->toPlainObject();
+ $db = $this->object->getConnection();
+
+ // TODO: Test this:
+ foreach ($this->hosts as $host) {
+ $service = IcingaService::fromPlainObject($plain, $db)
+ ->set('host_id', $host->get('id'));
+ $this->getDbObjectStore()->store($service);
+ }
+
+ $msg = sprintf(
+ $this->translate('The service "%s" has been added to %d hosts'),
+ $this->object->getObjectName(),
+ count($this->hosts)
+ );
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaAddServiceSetForm.php b/application/forms/IcingaAddServiceSetForm.php
new file mode 100644
index 0000000..b889110
--- /dev/null
+++ b/application/forms/IcingaAddServiceSetForm.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaAddServiceSetForm extends DirectorObjectForm
+{
+ /** @var IcingaHost[] */
+ private $hosts;
+
+ /** @var IcingaHost */
+ private $host;
+
+ /** @var IcingaServiceSet */
+ protected $object;
+
+ protected $objectName = 'service_set';
+
+ protected $listUrl = 'director/services/sets';
+
+ public function setup()
+ {
+ if ($this->object === null) {
+ $this->object = IcingaServiceSet::create(
+ ['object_type' => 'object'],
+ $this->db
+ );
+ }
+
+ $object = $this->object();
+ if ($this->hasBeenSent()) {
+ $object->set('object_name', $this->getSentValue('imports'));
+ $object->set('imports', $object->getObjectName());
+ }
+
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->addSingleImportsElement();
+ }
+
+ if (count($object->get('imports'))) {
+ $description = $object->getResolvedProperty('description');
+ if ($description) {
+ $this->addHtmlHint($description);
+ }
+ }
+
+ $this->addHidden('object_type', 'object');
+ $this->setButtons();
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ if ($this->host) {
+ $this->setSuccessUrl(
+ 'director/host/services',
+ array('name' => $this->host->getObjectName())
+ );
+ } else {
+ parent::setObjectSuccessUrl();
+ }
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+ /**
+ * @param IcingaHost[] $hosts
+ * @return $this
+ */
+ public function setHosts(array $hosts)
+ {
+ $this->hosts = $hosts;
+ return $this;
+ }
+
+ protected function addSingleImportsElement()
+ {
+ $enum = $this->enumAllowedTemplates();
+
+ $this->addElement('select', 'imports', array(
+ 'label' => $this->translate('Service set'),
+ 'description' => $this->translate(
+ 'The service Set that should be assigned'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionallyAddFromEnum($enum),
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ if ($this->host !== null) {
+ $this->object->set('host_id', $this->host->get('id'));
+ parent::onSuccess();
+ return;
+ }
+
+ $plain = $this->object->toPlainObject();
+ $db = $this->object->getConnection();
+
+ foreach ($this->hosts as $host) {
+ IcingaServiceSet::fromPlainObject($plain, $db)
+ ->set('host_id', $host->get('id'))
+ ->store();
+ }
+
+ $msg = sprintf(
+ $this->translate('The Service Set "%s" has been added to %d hosts'),
+ $this->object->getObjectName(),
+ count($this->hosts)
+ );
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaApiUserForm.php b/application/forms/IcingaApiUserForm.php
new file mode 100644
index 0000000..eda0857
--- /dev/null
+++ b/application/forms/IcingaApiUserForm.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaApiUserForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'external_object');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ ));
+
+ $this->addElement('password', 'password', array(
+ 'label' => $this->translate('Password'),
+ 'required' => true,
+ ));
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/IcingaCloneObjectForm.php b/application/forms/IcingaCloneObjectForm.php
new file mode 100644
index 0000000..6ee99ba
--- /dev/null
+++ b/application/forms/IcingaCloneObjectForm.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Acl;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class IcingaCloneObjectForm extends DirectorForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ protected $baseObjectUrl;
+
+ /** @var Branch */
+ protected $branch;
+
+ public function setup()
+ {
+ $isBranch = $this->branch && $this->branch->isBranch();
+ $branchOnly = $this->object->get('id') === null;
+ if ($isBranch && $this->object instanceof IcingaObject && $this->object->isTemplate()) {
+ $this->addHtml(Hint::error($this->translate(
+ 'Templates cannot be cloned in Configuration Branches'
+ )));
+ $this->submitLabel = false;
+ return;
+ }
+ $name = $this->object->getObjectName();
+ $this->addElement('text', 'new_object_name', array(
+ 'label' => $this->translate('New name'),
+ 'required' => true,
+ 'value' => $name,
+ ));
+
+ if (!$branchOnly && Acl::instance()->hasPermission('director/admin')) {
+ $this->addElement('select', 'clone_type', array(
+ 'label' => 'Clone type',
+ 'required' => true,
+ 'multiOptions' => array(
+ 'equal' => $this->translate('Clone the object as is, preserving imports'),
+ 'flat' => $this->translate('Flatten all inherited properties, strip imports'),
+ )
+ ));
+ }
+
+ if (!$branchOnly && ($this->object instanceof IcingaHost
+ || $this->object instanceof IcingaServiceSet)
+ ) {
+ $this->addBoolean('clone_services', [
+ 'label' => $this->translate('Clone Services'),
+ 'description' => $this->translate(
+ 'Also clone single Services defined for this Host'
+ )
+ ], 'y');
+ }
+
+ if (!$branchOnly && $this->object instanceof IcingaHost) {
+ $this->addBoolean('clone_service_sets', [
+ 'label' => $this->translate('Clone Service Sets'),
+ 'description' => $this->translate(
+ 'Also clone single Service Sets defined for this Host'
+ )
+ ], 'y');
+ }
+
+ if ($this->object instanceof IcingaService) {
+ if ($this->object->get('service_set_id') !== null) {
+ $this->addElement('select', 'target_service_set', [
+ 'label' => $this->translate('Target Service Set'),
+ 'description' => $this->translate(
+ 'Clone this service to the very same or to another Service Set'
+ ),
+ 'multiOptions' => $this->enumServiceSets(),
+ 'value' => $this->object->get('service_set_id')
+ ]);
+ } elseif ($this->object->get('host_id') !== null) {
+ $this->addElement('text', 'target_host', [
+ 'label' => $this->translate('Target Host'),
+ 'description' => $this->translate(
+ 'Clone this service to the very same or to another Host'
+ ),
+ 'value' => $this->object->get('host'),
+ 'class' => "autosubmit director-suggest",
+ 'data-suggestion-context' => 'HostsAndTemplates',
+ ]);
+ }
+ }
+
+ if ($this->object->isTemplate() && $this->object->supportsFields()) {
+ $this->addBoolean('clone_fields', [
+ 'label' => $this->translate('Clone Template Fields'),
+ 'description' => $this->translate(
+ 'Also clone fields provided by this Template'
+ )
+ ], 'y');
+ }
+
+ $this->submitLabel = sprintf(
+ $this->translate('Clone "%s"'),
+ $name
+ );
+ }
+
+ public function setBranch(Branch $branch)
+ {
+ $this->branch = $branch;
+
+ return $this;
+ }
+
+ public function setObjectBaseUrl($url)
+ {
+ $this->baseObjectUrl = $url;
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ $table = $object->getTableName();
+ $type = $object->getShortTableName();
+ $connection = $object->getConnection();
+ $db = $connection->getDbAdapter();
+ $newName = $this->getValue('new_object_name');
+ $resolve = Acl::instance()->hasPermission('director/admin')
+ && $this->getValue('clone_type') === 'flat';
+
+ $msg = sprintf(
+ 'The %s "%s" has been cloned from "%s"',
+ $type,
+ $newName,
+ $object->getObjectName()
+ );
+
+ if ($object->isTemplate() && $this->branch && $this->branch->isBranch()) {
+ throw new IcingaException('Cloning templates is not available for Branches');
+ }
+
+ if ($object->isTemplate() && $object->getObjectName() === $newName) {
+ throw new IcingaException(
+ $this->translate('Name needs to be changed when cloning a Template')
+ );
+ }
+
+ $new = $object::fromPlainObject(
+ $object->toPlainObject($resolve),
+ $connection
+ )->set('object_name', $newName);
+
+ if ($new->isExternal()) {
+ $new->set('object_type', 'object');
+ }
+
+ if ($set = $this->getValue('target_service_set')) {
+ $new->set(
+ 'service_set_id',
+ IcingaServiceSet::loadWithAutoIncId((int) $set, $connection)->get('id')
+ );
+ } elseif ($host = $this->getValue('target_host')) {
+ $new->set('host', $host);
+ }
+
+ $services = [];
+ $sets = [];
+ if ($object instanceof IcingaHost) {
+ $new->set('api_key', null);
+ if ($this->getValue('clone_services') === 'y') {
+ $services = $object->fetchServices();
+ }
+ if ($this->getValue('clone_service_sets') === 'y') {
+ $sets = $object->fetchServiceSets();
+ }
+ } elseif ($object instanceof IcingaServiceSet) {
+ if ($this->getValue('clone_services') === 'y') {
+ $services = $object->fetchServices();
+ }
+ }
+ if ($this->getValue('clone_fields') === 'y') {
+ $fields = $db->fetchAll(
+ $db->select()
+ ->from($table . '_field')
+ ->where("${type}_id = ?", $object->get('id'))
+ );
+ } else {
+ $fields = [];
+ }
+
+ $store = new DbObjectStore($connection, $this->branch);
+ if ($store->store($new)) {
+ $newId = $new->get('id');
+ foreach ($services as $service) {
+ $clone = IcingaService::fromPlainObject(
+ $service->toPlainObject(),
+ $connection
+ );
+
+ if ($new instanceof IcingaHost) {
+ $clone->set('host_id', $newId);
+ } elseif ($new instanceof IcingaServiceSet) {
+ $clone->set('service_set_id', $newId);
+ }
+ $store->store($clone);
+ }
+
+ foreach ($sets as $set) {
+ $newSet = IcingaServiceSet::fromPlainObject(
+ $set->toPlainObject(),
+ $connection
+ )->set('host_id', $newId);
+ $store->store($newSet);
+ }
+
+ foreach ($fields as $row) {
+ $row->{"${type}_id"} = $newId;
+ $db->insert($table . '_field', (array) $row);
+ }
+
+ if ($new instanceof IcingaServiceSet) {
+ $this->setSuccessUrl(
+ 'director/serviceset',
+ $new->getUrlParams()
+ );
+ } else {
+ $this->setSuccessUrl(
+ $this->baseObjectUrl ?: 'director/' . strtolower($type),
+ $new->getUrlParams()
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ protected function enumServiceSets()
+ {
+ $db = $this->object->getConnection()->getDbAdapter();
+ return $db->fetchPairs(
+ $db->select()
+ ->from('icinga_service_set', ['id', 'object_name'])
+ ->order('object_name')
+ );
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaCommandArgumentForm.php b/application/forms/IcingaCommandArgumentForm.php
new file mode 100644
index 0000000..5dbef41
--- /dev/null
+++ b/application/forms/IcingaCommandArgumentForm.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaCommandArgumentForm extends DirectorObjectForm
+{
+ /** @var IcingaCommand */
+ protected $commandObject;
+
+ public function setCommandObject(IcingaCommand $object)
+ {
+ $this->commandObject = $object;
+ $this->setDb($object->getConnection());
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->addHidden('command_id', $this->commandObject->get('id'));
+
+ $this->addElement('text', 'argument_name', array(
+ 'label' => $this->translate('Argument name'),
+ 'filters' => array('StringTrim'),
+ 'description' => $this->translate('e.g. -H or --hostname, empty means "skip_key"')
+ ));
+
+ $this->addElement('text', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate('Description of the argument')
+ ));
+
+ $this->addElement('select', 'argument_format', array(
+ 'label' => $this->translate('Value type'),
+ 'multiOptions' => array(
+ 'string' => $this->translate('String'),
+ 'expression' => $this->translate('Icinga DSL')
+ ),
+ 'description' => $this->translate(
+ 'Whether the argument value is a string (allowing macros like $host$)'
+ . ' or an Icinga DSL lambda function (will be enclosed with {{ ... }}'
+ ),
+ 'class' => 'autosubmit',
+ ));
+
+ if ($this->getSentOrObjectValue('argument_format') === 'expression') {
+ $this->addElement('textarea', 'argument_value', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate(
+ 'An Icinga DSL expression, e.g.: var cmd = macro("$cmd$");'
+ . ' return typeof(command) == String ...'
+ ),
+ 'rows' => 3
+ ));
+ } else {
+ $this->addElement('text', 'argument_value', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate(
+ 'e.g. 5%, $host.name$, $lower$%:$upper$%'
+ )
+ ));
+ }
+
+ $this->addElement('text', 'sort_order', array(
+ 'label' => $this->translate('Position'),
+ 'description' => $this->translate(
+ 'Leave empty for non-positional arguments. Can be a positive or'
+ . ' negative number and influences argument ordering'
+ )
+ ));
+
+ $this->addElement('select', 'set_if_format', array(
+ 'label' => $this->translate('Condition format'),
+ 'multiOptions' => array(
+ 'string' => $this->translate('String'),
+ 'expression' => $this->translate('Icinga DSL')
+ ),
+ 'description' => $this->translate(
+ 'Whether the set_if parameter is a string (allowing macros like $host$)'
+ . ' or an Icinga DSL lambda function (will be enclosed with {{ ... }}'
+ ),
+ 'class' => 'autosubmit',
+ ));
+
+ if ($this->getSentOrObjectValue('set_if_format') === 'expression') {
+ $this->addElement('textarea', 'set_if', array(
+ 'label' => $this->translate('Condition (set_if)'),
+ 'description' => $this->translate(
+ 'An Icinga DSL expression that returns a boolean value, e.g.: var cmd = bool(macro("$cmd$"));'
+ . ' return cmd ...'
+ ),
+ 'rows' => 3
+ ));
+ } else {
+ $this->addElement('text', 'set_if', array(
+ 'label' => $this->translate('Condition (set_if)'),
+ 'description' => $this->translate(
+ 'Only set this parameter if the argument value resolves to a'
+ . ' numeric value. String values are not supported'
+ )
+ ));
+ }
+
+ $this->addBoolean('repeat_key', array(
+ 'label' => $this->translate('Repeat key'),
+ 'description' => $this->translate(
+ 'Whether this parameter should be repeated when multiple values'
+ . ' (read: array) are given'
+ )
+ ));
+
+ $this->addBoolean('skip_key', array(
+ 'label' => $this->translate('Skip key'),
+ 'description' => $this->translate(
+ 'Whether the parameter name should not be passed to the command.'
+ . ' Per default, the parameter name (e.g. -H) will be appended,'
+ . ' so no need to explicitly set this to "No".'
+ )
+ ));
+
+ $this->addBoolean('required', array(
+ 'label' => $this->translate('Required'),
+ 'required' => false,
+ 'description' => $this->translate('Whether this argument should be required')
+ ));
+
+ $this->setButtons();
+ }
+
+ protected function deleteObject($object)
+ {
+ $cmd = $this->commandObject;
+
+ $msg = sprintf(
+ $this->translate('%s argument "%s" has been removed'),
+ $this->translate($this->getObjectShortClassName()),
+ $object->argument_name
+ );
+
+ // TODO: remove argument_id, once verified that it is no longer in use
+ $url = $this->getSuccessUrl()->without('argument_id')->without('argument');
+
+ $cmd->arguments()->remove($object->argument_name);
+ if ($this->branch->isBranch()) {
+ $this->getDbObjectStore()->store($cmd);
+ $this->setSuccessUrl($url);
+ } else {
+ if ($cmd->store()) {
+ $this->setSuccessUrl($url);
+ }
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object();
+ $cmd = $this->commandObject;
+ if ($object->get('argument_name') === null) {
+ $object->set('skip_key', true);
+ $object->set('argument_name', $cmd->getNextSkippableKeyName());
+ }
+
+ if ($object->hasBeenModified()) {
+ $cmd->arguments()->set(
+ $object->get('argument_name'),
+ $object
+ );
+ $msg = sprintf(
+ $this->translate('The argument %s has successfully been stored'),
+ $object->get('argument_name')
+ );
+ $this->getDbObjectStore()->store($cmd);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+ $this->setSuccessUrl('director/command/arguments', [
+ 'argument' => $object->get('argument_name'),
+ 'name' => $cmd->getObjectName()
+ ]);
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaCommandForm.php b/application/forms/IcingaCommandForm.php
new file mode 100644
index 0000000..ba1386b
--- /dev/null
+++ b/application/forms/IcingaCommandForm.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaCommandForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ return;
+ }
+
+ $this->addElement('select', 'methods_execute', array(
+ 'label' => $this->translate('Command type'),
+ 'multiOptions' => array(
+ null => '- please choose -',
+ $this->translate('Plugin commands') => array(
+ 'PluginCheck' => 'Plugin Check Command',
+ 'PluginNotification' => 'Notification Plugin Command',
+ 'PluginEvent' => 'Event Plugin Command',
+ ),
+ $this->translate('Internal commands') => array(
+ 'IcingaCheck' => 'Icinga Check Command',
+ 'ClusterCheck' => 'Icinga Cluster Check Command',
+ 'ClusterZoneCheck' => 'Icinga Cluster Zone Check Command',
+ 'IdoCheck' => 'Ido Check Command',
+ 'RandomCheck' => 'Random Check Command',
+ )
+ ),
+ 'required' => ! $this->isTemplate(),
+ 'description' => $this->translate(
+ 'Plugin Check commands are what you need when running checks agains'
+ . ' your infrastructure. Notification commands will be used when it'
+ . ' comes to notify your users. Event commands allow you to trigger'
+ . ' specific actions when problems occur. Some people use them for'
+ . ' auto-healing mechanisms, like restarting services or rebooting'
+ . ' systems at specific thresholds'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ $nameLabel = $this->isTemplate()
+ ? $this->translate('Name')
+ : $this->translate('Command name');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $nameLabel,
+ 'required' => true,
+ 'description' => $this->translate('Identifier for the Icinga command you are going to create')
+ ));
+
+ $this->addImportsElement(false);
+
+ $this->addElement('text', 'command', array(
+ 'label' => $this->translate('Command'),
+ 'required' => ! $this->isTemplate(),
+ 'description' => $this->translate(
+ 'The command Icinga should run. Absolute paths are accepted as provided,'
+ . ' relative paths are prefixed with "PluginDir + ", similar Constant prefixes are allowed.'
+ . ' Spaces will lead to separation of command path and standalone arguments. Please note that'
+ . ' this means that we do not support spaces in plugin names and paths right now.'
+ )
+ ));
+
+ $this->addElement('text', 'timeout', array(
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate(
+ 'Optional command timeout. Allowed values are seconds or durations postfixed with a'
+ . ' specific unit (e.g. 1m or also 3m 30s).'
+ )
+ ));
+
+ $descIsString = [
+ $this->translate('Render the command as a plain string instead of an array.'),
+ $this->translate('If enabled you can not define arguments.'),
+ $this->translate('Disabled by default, and should only be used in rare cases.'),
+ $this->translate('WARNING, this can allow shell script injection via custom variables used in command.'),
+ ];
+
+ $this->addBoolean(
+ 'is_string',
+ array(
+ 'label' => $this->translate('Render as string'),
+ 'description' => join(' ', $descIsString),
+ )
+ );
+
+ $this->addDisabledElement();
+ $this->addZoneSection();
+ $this->setButtons();
+ }
+
+ protected function addZoneSection()
+ {
+ $this->addZoneElement(true);
+
+ $elements = array(
+ 'zone_id',
+ );
+ $this->addDisplayGroup($elements, 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ protected function enumAllowedTemplates()
+ {
+ $object = $this->object();
+ $tpl = $this->db->enum($object->getTableName());
+ if (empty($tpl)) {
+ return array();
+ }
+
+ $id = $object->get('id');
+
+ if (array_key_exists($id, $tpl)) {
+ unset($tpl[$id]);
+ }
+
+ if (empty($tpl)) {
+ return array();
+ }
+
+ $tpl = array_combine($tpl, $tpl);
+ return $tpl;
+ }
+}
diff --git a/application/forms/IcingaDeleteObjectForm.php b/application/forms/IcingaDeleteObjectForm.php
new file mode 100644
index 0000000..409bdc3
--- /dev/null
+++ b/application/forms/IcingaDeleteObjectForm.php
@@ -0,0 +1,41 @@
+<?php
+
+// TODO: Check whether this can be removed
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class IcingaDeleteObjectForm extends QuickForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ public function setup()
+ {
+ $this->submitLabel = sprintf(
+ $this->translate('YES, please delete "%s"'),
+ $this->object->getObjectName()
+ );
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ $msg = sprintf(
+ 'The %s "%s" has been deleted',
+ $object->getShortTableName(),
+ $object->getObjectName()
+ );
+
+ if ($object->delete()) {
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaDependencyForm.php b/application/forms/IcingaDependencyForm.php
new file mode 100644
index 0000000..ab30844
--- /dev/null
+++ b/application/forms/IcingaDependencyForm.php
@@ -0,0 +1,309 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaDependency;
+
+class IcingaDependencyForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->setupDependencyElements();
+ }
+
+ /***
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupDependencyElements()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->addNameElement()
+ ->addDisabledElement()
+ ->addImportsElement()
+ ->addObjectsElement()
+ ->addBooleanElements()
+ ->addPeriodElement()
+ ->addAssignmentElements()
+ ->addEventFilterElements(['states'])
+ ->groupMainProperties()
+ ->addZoneSection()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addZoneSection()
+ {
+ $this->addZoneElement(true);
+
+ $elements = array(
+ 'zone_id',
+ );
+ $this->addDisplayGroup($elements, 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addNameElement()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga dependency you are going to create')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAssignmentElements()
+ {
+ if (!$this->object || !$this->object->isApplyRule()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'apply_to', [
+ 'label' => $this->translate('Apply to'),
+ 'description' => $this->translate(
+ 'Whether this dependency should affect hosts or services'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum([
+ 'host' => $this->translate('Hosts'),
+ 'service' => $this->translate('Services'),
+ ])
+ ]);
+
+ $applyTo = $this->getSentOrObjectValue('apply_to');
+
+ if (! $applyTo) {
+ return $this;
+ }
+
+ $suggestionContext = ucfirst($applyTo) . 'FilterColumns';
+ $this->addAssignFilter([
+ 'suggestionContext' => $suggestionContext,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addPeriodElement()
+ {
+ $periods = $this->db->enumTimeperiods();
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'select',
+ 'period_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when this'
+ . ' notification should be triggered. Not set by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addBooleanElements()
+ {
+ $this->addBoolean('disable_checks', [
+ 'label' => $this->translate('Disable Checks'),
+ 'description' => $this->translate(
+ 'Whether to disable checks when this dependency fails.'
+ . ' Defaults to false.'
+ )
+ ], null);
+
+ $this->addBoolean('disable_notifications', [
+ 'label' => $this->translate('Disable Notificiations'),
+ 'description' => $this->translate(
+ 'Whether to disable notifications when this dependency fails.'
+ . ' Defaults to true.'
+ )
+ ], null);
+
+ $this->addBoolean('ignore_soft_states', [
+ 'label' => $this->translate('Ignore Soft States'),
+ 'description' => $this->translate(
+ 'Whether to ignore soft states for the reachability calculation.'
+ . ' Defaults to true.'
+ )
+ ], null);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addObjectsElement()
+ {
+ $dependency = $this->getObject();
+ $parentHost = $dependency->get('parent_host');
+ if ($parentHost === null) {
+ $parentHostVar = $dependency->get('parent_host_var');
+ if (\strlen($parentHostVar) > 0) {
+ $parentHost = '$' . $dependency->get('parent_host_var') . '$';
+ }
+ }
+ $this->addElement('text', 'parent_host', [
+ 'label' => $this->translate('Parent Host'),
+ 'description' => $this->translate(
+ 'The parent host. You might want to refer Host Custom Variables'
+ . ' via $host.vars.varname$'
+ ),
+ 'class' => "autosubmit director-suggest",
+ 'data-suggestion-context' => 'hostnames',
+ 'order' => 10,
+ 'required' => $this->isObject(),
+ 'value' => $parentHost
+ ]);
+ $sentParent = $this->getSentOrObjectValue('parent_host');
+
+ if (!empty($sentParent) || $dependency->isApplyRule()) {
+ $parentService = $dependency->get('parent_service');
+ $this->addElement('text', 'parent_service', [
+ 'label' => $this->translate('Parent Service'),
+ 'description' => $this->translate(
+ 'Optional. The parent service. If omitted this dependency'
+ . ' object is treated as host dependency.'
+ ),
+ 'class' => "autosubmit director-suggest",
+ 'data-suggestion-context' => 'servicenames',
+ 'data-suggestion-for-host' => $sentParent,
+ 'order' => 20,
+ 'value' => $parentService
+ ]);
+ }
+
+ // If configuring Object, allow selection of child host and/or service,
+ // otherwise apply rules will determine child object.
+ if ($dependency->isObject()) {
+ $this->addElement('text', 'child_host', [
+ 'label' => $this->translate('Child Host'),
+ 'description' => $this->translate('The child host.'),
+ 'value' => $dependency->get('child_host'),
+ 'order' => 30,
+ 'class' => 'autosubmit director-suggest',
+ 'required' => $this->isObject(),
+ 'data-suggestion-context' => 'hostnames',
+ ]);
+
+ $sentChild = $this->getSentOrObjectValue('child_host');
+
+ if (!empty($sentChild)) {
+ $this->addElement('text', 'child_service', [
+ 'label' => $this->translate('Child Service'),
+ 'description' => $this->translate(
+ 'Optional. The child service. If omitted this dependency'
+ . ' object is treated as host dependency.'
+ ),
+ 'class' => 'autosubmit director-suggest',
+ 'order' => 40,
+ 'value' => $this->getObject()->get('child_service'),
+ 'data-suggestion-context' => 'servicenames',
+ 'data-suggestion-for-host' => $sentChild,
+ ]);
+ }
+ }
+
+ $elements = ['parent_host', 'child_host', 'parent_service', 'child_service'];
+ $this->addDisplayGroup($elements, 'related_objects', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_RELATED_OBJECTS,
+ 'legend' => $this->translate('Related Objects')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Hint: this is unused. Why?
+ *
+ * @param IcingaDependency $dependency
+ * @return $this
+ */
+ public function createApplyRuleFor(IcingaDependency $dependency)
+ {
+ $object = $this->object();
+ $object->setImports($dependency->getObjectName());
+ $object->set('object_type', 'apply');
+ $object->set('object_name', $dependency->getObjectName());
+
+ return $this;
+ }
+
+ protected function handleProperties(DbObject $object, &$values)
+ {
+ if ($this->hasBeenSent()) {
+ if (isset($values['parent_host'])
+ && $this->isCustomVar($values['parent_host'])
+ ) {
+ $values['parent_host_var'] = \trim($values['parent_host'], '$');
+ $values['parent_host'] = '';
+ }
+ }
+
+ parent::handleProperties($object, $values);
+ }
+
+ protected function isCustomVar($string)
+ {
+ return \preg_match('/^\$(?:host)\.vars\..+\$$/', $string);
+ // Eventually: return \preg_match('/^\$(?:host|service)\.vars\..+\$$/', $string);
+ }
+}
diff --git a/application/forms/IcingaEndpointForm.php b/application/forms/IcingaEndpointForm.php
new file mode 100644
index 0000000..1c08cb4
--- /dev/null
+++ b/application/forms/IcingaEndpointForm.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaEndpointForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ return;
+ }
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Endpoint template name'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga endpoint template you are going to create')
+ ));
+ } else {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Endpoint'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga endpoint you are going to create')
+ ));
+ }
+
+ $this->addElement('text', 'host', array(
+ 'label' => $this->translate('Endpoint address'),
+ 'description' => $this->translate('IP address / hostname of remote node')
+ ));
+
+ $this->addElement('text', 'port', array(
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the endpoint.'),
+ ));
+
+ $this->addElement('text', 'log_duration', array(
+ 'label' => $this->translate('Log Duration'),
+ 'description' => $this->translate('The log duration time.')
+ ));
+
+ $this->addElement('select', 'apiuser_id', array(
+ 'label' => $this->translate('API user'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumApiUsers())
+ ));
+
+ $this->addZoneElement();
+
+ if ($this->object->hasBeenLoadedFromDb()) {
+ $imports = $this->object->get('imports');
+ if ($imports !== null && count($imports) > 0) {
+ $this->addImportsElement(false);
+ }
+ }
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/IcingaForgetApiKeyForm.php b/application/forms/IcingaForgetApiKeyForm.php
new file mode 100644
index 0000000..d1f475c
--- /dev/null
+++ b/application/forms/IcingaForgetApiKeyForm.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class IcingaForgetApiKeyForm extends DirectorForm
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->addStandaloneSubmitButton(sprintf(
+ $this->translate('Drop Self Service API key'),
+ $this->host->getObjectName()
+ ));
+ }
+
+ public function onSuccess()
+ {
+ $this->host->set('api_key', null)->store();
+ $this->redirectOnSuccess(sprintf($this->translate(
+ 'The Self Service API key for %s has been dropped'
+ ), $this->host->getObjectName()));
+ }
+}
diff --git a/application/forms/IcingaGenerateApiKeyForm.php b/application/forms/IcingaGenerateApiKeyForm.php
new file mode 100644
index 0000000..18980f0
--- /dev/null
+++ b/application/forms/IcingaGenerateApiKeyForm.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class IcingaGenerateApiKeyForm extends DirectorForm
+{
+ /** @var IcingaHost */
+ protected $host;
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ public function setup()
+ {
+ if ($this->host->getProperty('api_key')) {
+ $label = $this->translate('Regenerate Self Service API key');
+ } else {
+ $label = $this->translate('Generate Self Service API key');
+ }
+
+ $this->addStandaloneSubmitButton(sprintf(
+ $label,
+ $this->host->getObjectName()
+ ));
+ }
+
+ public function onSuccess()
+ {
+ $host = $this->host;
+ $host->generateApiKey();
+ $host->store();
+ $this->redirectOnSuccess(sprintf($this->translate(
+ 'A new Self Service API key for %s has been generated'
+ ), $host->getObjectName()));
+ }
+}
diff --git a/application/forms/IcingaHostForm.php b/application/forms/IcingaHostForm.php
new file mode 100644
index 0000000..ec71471
--- /dev/null
+++ b/application/forms/IcingaHostForm.php
@@ -0,0 +1,390 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\AuthenticationException;
+use Icinga\Module\Director\Repository\IcingaTemplateRepository;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+
+class IcingaHostForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $simpleImports = $this->isNew() && ! $this->isTemplate();
+ if ($simpleImports) {
+ if (!$this->addSingleImportElement(true)) {
+ $this->setSubmitLabel(false);
+ return;
+ }
+
+ if (! ($imports = $this->getSentOrObjectValue('imports'))) {
+ $this->setSubmitLabel($this->translate('Next'));
+ $this->groupMainProperties();
+ return;
+ }
+ }
+
+ $nameLabel = $this->isTemplate()
+ ? $this->translate('Name')
+ : $this->translate('Hostname');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $nameLabel,
+ 'required' => true,
+ 'spellcheck' => 'false',
+ 'description' => $this->translate(
+ 'Icinga object name for this host. This is usually a fully qualified host name'
+ . ' but it could basically be any kind of string. To make things easier for your'
+ . ' users we strongly suggest to use meaningful names for templates. E.g. "generic-host"'
+ . ' is ugly, "Standard Linux Server" is easier to understand'
+ )
+ ));
+
+ if (! $simpleImports) {
+ $this->addImportsElement();
+ }
+
+ $this->addChoices('host')
+ ->addDisplayNameElement()
+ ->addAddressElements()
+ ->addGroupsElement()
+ ->addDisabledElement()
+ ->groupMainProperties($simpleImports)
+ ->addCheckCommandElements()
+ ->addCheckExecutionElements()
+ ->addExtraInfoElements()
+ ->addClusteringElements()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addClusteringElements()
+ {
+ $this->addZoneElement();
+ $this->addBoolean('has_agent', [
+ 'label' => $this->translate('Icinga2 Agent'),
+ 'description' => $this->translate(
+ 'Whether this host has the Icinga 2 Agent installed'
+ ),
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($this->getSentOrResolvedObjectValue('has_agent') === 'y') {
+ $this->addBoolean('master_should_connect', [
+ 'label' => $this->translate('Establish connection'),
+ 'description' => $this->translate(
+ 'Whether the parent (master) node should actively try to connect to this agent'
+ ),
+ 'required' => true
+ ]);
+ $this->addBoolean('accept_config', [
+ 'label' => $this->translate('Accepts config'),
+ 'description' => $this->translate('Whether the agent is configured to accept config'),
+ 'required' => true
+ ]);
+
+ $this->addHidden('command_endpoint_id', null);
+ $this->setSentValue('command_endpoint_id', null);
+
+ $settings = $this->object->getConnection()->settings();
+ if ($settings->get('feature_custom_endpoint') === 'y' && ! $this->isTemplate()) {
+ $this->addElement('text', 'custom_endpoint_name', [
+ 'label' => $this->translate('Custom Endpoint Name'),
+ 'description' => $this->translate(
+ 'Use a different name for the generated endpoint object than the host name'
+ . ' and add a custom variable to allow services setting the correct command endpoint.'
+ ),
+ ]);
+ }
+ } else {
+ if ($this->isTemplate()) {
+ $this->addElement('select', 'command_endpoint_id', [
+ 'label' => $this->translate('Command endpoint'),
+ 'description' => $this->translate(
+ 'Setting a command endpoint allows you to force host checks'
+ . ' to be executed by a specific endpoint. Please carefully'
+ . ' study the related Icinga documentation before using this'
+ . ' feature'
+ ),
+ 'multiOptions' => $this->optionalEnum($this->enumEndpoints())
+ ]);
+ }
+
+ foreach (['master_should_connect', 'accept_config'] as $key) {
+ $this->addHidden($key, null);
+ $this->setSentValue($key, null);
+ }
+ }
+
+ $elements = [
+ 'zone_id',
+ 'has_agent',
+ 'master_should_connect',
+ 'accept_config',
+ 'command_endpoint_id',
+ 'custom_endpoint_name',
+ 'api_key',
+ ];
+ $this->addDisplayGroup($elements, 'clustering', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Icinga Agent and zone settings')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @param bool $required
+ * @return bool
+ */
+ protected function addSingleImportElement($required = null)
+ {
+ $enum = $this->enumHostTemplates();
+ if (empty($enum)) {
+ if ($required) {
+ if ($this->hasBeenSent()) {
+ $this->addError($this->translate('No Host template has been chosen'));
+ } else {
+ if ($this->hasPermission('director/admin')) {
+ $html = sprintf(
+ $this->translate('Please define a %s first'),
+ Link::create(
+ $this->translate('Host Template'),
+ 'director/host/add',
+ ['type' => 'template']
+ )
+ );
+ } else {
+ $html = $this->translate('No Host Template has been provided yet');
+ }
+
+ $this->addHtml('<p class="warning">' . $html . '</p>');
+ }
+ }
+
+ return false;
+ }
+
+ $this->addElement('select', 'imports', [
+ 'label' => $this->translate('Host Template'),
+ 'description' => $this->translate(
+ 'Choose a Host Template'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($enum),
+ 'class' => 'autosubmit'
+ ]);
+
+ return true;
+ }
+
+ protected function enumHostTemplates()
+ {
+ $tpl = IcingaTemplateRepository::instanceByType('host', $this->getDb())
+ ->listAllowedTemplateNames();
+ return array_combine($tpl, $tpl);
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addGroupsElement()
+ {
+ if ($this->hasHostGroupRestriction()
+ && ! $this->getAuth()->hasPermission('director/groups-for-restricted-hosts')
+ ) {
+ return $this;
+ }
+
+ $this->addElement('extensibleSet', 'groups', array(
+ 'label' => $this->translate('Groups'),
+ 'suggest' => 'hostgroupnames',
+ 'description' => $this->translate(
+ 'Hostgroups that should be directly assigned to this node. Hostgroups can be useful'
+ . ' for various reasons. You might assign service checks based on assigned hostgroup.'
+ . ' They are also often used as an instrument to enforce restricted views in Icinga Web 2.'
+ . ' Hostgroups can be directly assigned to single hosts or to host templates. You might'
+ . ' also want to consider assigning hostgroups using apply rules'
+ )
+ ));
+
+ $applied = $this->getAppliedGroups();
+ if (! empty($applied)) {
+ $this->addElement('simpleNote', 'applied_groups', [
+ 'label' => $this->translate('Applied groups'),
+ 'value' => $this->createHostgroupLinks($applied),
+ 'ignore' => true,
+ ]);
+ }
+
+ $inherited = $this->getInheritedGroups();
+ if (! empty($inherited)) {
+ /** @var BaseHtmlElement $links */
+ $links = $this->createHostgroupLinks($inherited);
+ if (count($this->object()->getGroups())) {
+ $links->addAttributes(['class' => 'strike-links']);
+ /** @var BaseHtmlElement $link */
+ foreach ($links->getContent() as $link) {
+ if ($link instanceof BaseHtmlElement) {
+ $link->addAttributes([
+ 'title' => $this->translate(
+ 'Group has been inherited, but will be overridden'
+ . ' by locally assigned group(s)'
+ )
+ ]);
+ }
+ }
+ }
+ $this->addElement('simpleNote', 'inherited_groups', [
+ 'label' => $this->translate('Inherited groups'),
+ 'value' => $links,
+ 'ignore' => true,
+ ]);
+ }
+
+ return $this;
+ }
+
+ protected function strikeGroupLinks(BaseHtmlElement $links)
+ {
+ /** @var BaseHtmlElement $link */
+ foreach ($links->getContent() as $link) {
+ $link->getAttributes()->add('style', 'text-decoration: strike');
+ }
+ $links->add('aha');
+ }
+
+ protected function getInheritedGroups()
+ {
+ if ($this->hasObject()) {
+ return $this->object->listInheritedGroupNames();
+ } else {
+ return [];
+ }
+ }
+
+ protected function createHostgroupLinks($groups)
+ {
+ $links = [];
+ foreach ($groups as $name) {
+ if (! empty($links)) {
+ $links[] = ', ';
+ }
+ $links[] = Link::create(
+ $name,
+ 'director/hostgroup',
+ ['name' => $name],
+ ['data-base-target' => '_next']
+ );
+ }
+
+ return Html::tag('span', [
+ 'style' => 'line-height: 2.5em; padding-left: 0.5em'
+ ], $links);
+ }
+
+ protected function getAppliedGroups()
+ {
+ if ($this->isNew()) {
+ return [];
+ }
+
+ return $this->object()->getAppliedGroups();
+ }
+
+ protected function hasHostGroupRestriction()
+ {
+ return $this->getAuth()->getRestrictions('director/filter/hostgroups');
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addAddressElements()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'address', array(
+ 'label' => $this->translate('Host address'),
+ 'description' => $this->translate(
+ 'Host address. Usually an IPv4 address, but may be any kind of address'
+ . ' your check plugin is able to deal with'
+ )
+ ));
+
+ $this->addElement('text', 'address6', array(
+ 'label' => $this->translate('IPv6 address'),
+ 'description' => $this->translate('Usually your hosts main IPv6 address')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addDisplayNameElement()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Display name'),
+ 'spellcheck' => 'false',
+ 'description' => $this->translate(
+ 'Alternative name for this host. Might be a host alias or and kind'
+ . ' of string helping your users to identify this host'
+ )
+ ));
+
+ return $this;
+ }
+
+ protected function enumEndpoints()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from('icinga_endpoint', [
+ 'id',
+ 'object_name'
+ ])->where(
+ 'object_type IN (?)',
+ ['object', 'external_object']
+ )->order('object_name');
+
+ return $db->fetchPairs($select);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->hasHostGroupRestriction()) {
+ $restriction = new HostgroupRestriction($this->getDb(), $this->getAuth());
+ if (! $restriction->allowsHost($this->object())) {
+ throw new AuthenticationException($this->translate(
+ 'Unable to store a host with the given properties because of insufficient permissions'
+ ));
+ }
+ }
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/IcingaHostGroupForm.php b/application/forms/IcingaHostGroupForm.php
new file mode 100644
index 0000000..be48318
--- /dev/null
+++ b/application/forms/IcingaHostGroupForm.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaHostGroupForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Hostgroup'),
+ 'required' => true,
+ 'description' => $this->translate('Icinga object name for this host group')
+ ]);
+
+ $this->addGroupDisplayNameElement()
+ ->addAssignmentElements()
+ ->setButtons();
+ }
+
+ protected function addAssignmentElements()
+ {
+ $this->addAssignFilter([
+ 'suggestionContext' => 'HostFilterColumns',
+ 'required' => false,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaHostSelfServiceForm.php b/application/forms/IcingaHostSelfServiceForm.php
new file mode 100644
index 0000000..1e05b96
--- /dev/null
+++ b/application/forms/IcingaHostSelfServiceForm.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use Icinga\Security\SecurityException;
+
+class IcingaHostSelfServiceForm extends DirectorForm
+{
+ /** @var string */
+ private $hostApiKey;
+
+ /** @var IcingaHost */
+ private $template;
+
+ private $hostName;
+
+ public function setup()
+ {
+ if ($this->hostName === null) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Host name'),
+ 'required' => true,
+ 'value' => $this->hostName,
+ ));
+ }
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Alias'),
+ ));
+
+ $this->addElement('text', 'address', array(
+ 'label' => $this->translate('Host address'),
+ 'description' => $this->translate(
+ 'Host address. Usually an IPv4 address, but may be any kind of address'
+ . ' your check plugin is able to deal with'
+ )
+ ));
+
+ $this->addElement('text', 'address6', array(
+ 'label' => $this->translate('IPv6 address'),
+ 'description' => $this->translate('Usually your hosts main IPv6 address')
+ ));
+
+ if ($this->template === null) {
+ $this->addElement('text', 'key', array(
+ 'label' => $this->translate('API Key'),
+ 'ignore' => true,
+ 'required' => true,
+ ));
+ }
+
+ $this->submitLabel = sprintf(
+ $this->translate('Register')
+ );
+ }
+
+ public function setHostName($name)
+ {
+ $this->hostName = $name;
+ $this->removeElement('object_name');
+ return $this;
+ }
+
+ public function loadTemplateWithApiKey($key)
+ {
+ $this->template = IcingaHost::loadWithApiKey($key, $this->getDb());
+ if (! $this->template->isTemplate()) {
+ throw new NotFoundError('Got invalid API key "%s"', $key);
+ }
+
+ if ($this->template->getResolvedProperty('has_agent') !== 'y') {
+ throw new NotFoundError(
+ 'Got valid API key "%s", but template is not for Agents',
+ $key
+ );
+ }
+
+ $this->removeElement('key');
+
+ return $this->template;
+ }
+
+ public function listMissingRequiredFields()
+ {
+ $result = [];
+ foreach ($this->getElements() as $element) {
+ if (in_array('isEmpty', $element->getErrors())) {
+ $result[] = $element->getName();
+ }
+ }
+
+ return $result;
+ }
+
+ public function isMissingRequiredFields()
+ {
+ return count($this->listMissingRequiredFields()) > 0;
+ }
+
+ public function onSuccess()
+ {
+ $db = $this->getDb();
+ if ($this->template === null) {
+ $this->loadTemplateWithApiKey($this->getValue('key'));
+ }
+ $name = $this->hostName ?: $this->getValue('object_name');
+ if (IcingaHost::exists($name, $db)) {
+ $host = IcingaHost::load($name, $db);
+ if ($host->isTemplate()) {
+ throw new SecurityException(
+ 'You are not allowed to create "%s"',
+ $name
+ );
+ }
+
+ if (null !== $host->getProperty('api_key')) {
+ throw new SecurityException(
+ 'The host "%s" has already been registered',
+ $name
+ );
+ }
+
+ $propertyNames = ['display_name', 'address', 'address6'];
+ foreach ($propertyNames as $property) {
+ if (\strlen($value = $this->getValue($property)) > 0) {
+ $host->set($property, $value);
+ }
+ }
+ } else {
+ $host = IcingaHost::create(array_filter($this->getValues(), 'strlen'), $db);
+ $host->set('object_name', $name);
+ $host->set('object_type', 'object');
+ $host->set('imports', [$this->template]);
+ }
+
+ $key = $host->generateApiKey();
+ $host->store($db);
+ $this->hostApiKey = $key;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getHostApiKey()
+ {
+ return $this->hostApiKey;
+ }
+
+ public static function create(Db $db)
+ {
+ return static::load()->setDb($db);
+ }
+}
diff --git a/application/forms/IcingaHostVarForm.php b/application/forms/IcingaHostVarForm.php
new file mode 100644
index 0000000..cb15bcb
--- /dev/null
+++ b/application/forms/IcingaHostVarForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+/**
+ * @deprecated
+ */
+class IcingaHostVarForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('select', 'host_id', array(
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The name of the host'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumHosts()),
+ 'required' => true
+ ));
+
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate('host var name')
+ ));
+
+ $this->addElement('textarea', 'varvalue', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate('host var value')
+ ));
+
+ $this->addElement('text', 'format', array(
+ 'label' => $this->translate('Format'),
+ 'description' => $this->translate('value format')
+ ));
+ }
+}
diff --git a/application/forms/IcingaImportObjectForm.php b/application/forms/IcingaImportObjectForm.php
new file mode 100644
index 0000000..3942f74
--- /dev/null
+++ b/application/forms/IcingaImportObjectForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class IcingaImportObjectForm extends QuickForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ public function setup()
+ {
+ $this->addNote($this->translate(
+ "Importing an object means that its type will change from"
+ . ' "external" to "object". That way it will make part of the'
+ . ' next deployment. So in case you imported this object from'
+ . ' your Icinga node make sure to remove it from your local'
+ . ' configuration before issueing the next deployment. In case'
+ . ' of a conflict nothing bad will happen, just your config'
+ . " won't deploy."
+ ));
+
+ $this->submitLabel = sprintf(
+ $this->translate('Import external "%s"'),
+ $this->object->object_name
+ );
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ if ($object->set('object_type', 'object')->store()) {
+ $this->redirectOnSuccess(sprintf(
+ $this->translate('%s "%s" has been imported"'),
+ $object->getShortTableName(),
+ $object->getObjectName()
+ ));
+ } else {
+ $this->addError(sprintf(
+ $this->translate('Failed to import %s "%s"'),
+ $object->getShortTableName(),
+ $object->getObjectName()
+ ));
+ }
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaMultiEditForm.php b/application/forms/IcingaMultiEditForm.php
new file mode 100644
index 0000000..4149a70
--- /dev/null
+++ b/application/forms/IcingaMultiEditForm.php
@@ -0,0 +1,324 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+use Icinga\Module\Director\Hook\IcingaObjectFormHook;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Zend_Form_Element as ZfElement;
+
+class IcingaMultiEditForm extends DirectorObjectForm
+{
+ /** @var DbObject[] */
+ private $objects;
+
+ private $elementGroupMap;
+
+ /** @var QuickForm */
+ private $relatedForm;
+
+ private $propertiesToPick;
+
+ public function setObjects($objects)
+ {
+ $this->objects = $objects;
+ $this->object = current($this->objects);
+ $this->db = $this->object()->getConnection();
+ return $this;
+ }
+
+ public function isMultiObjectForm()
+ {
+ return true;
+ }
+
+ public function pickElementsFrom(QuickForm $form, $properties)
+ {
+ $this->relatedForm = $form;
+ $this->propertiesToPick = $properties;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $object = $this->object;
+
+ $loader = new IcingaObjectFieldLoader($object);
+ $loader->prepareElements($this);
+ $loader->addFieldsToForm($this);
+
+ if ($form = $this->relatedForm) {
+ if ($form instanceof DirectorObjectForm) {
+ $form->setDb($object->getConnection())
+ ->setObject($object);
+ }
+
+ $form->prepareElements();
+ } else {
+ $this->propertiesToPick = array();
+ }
+
+ foreach ($this->propertiesToPick as $property) {
+ if ($el = $form->getElement($property)) {
+ $this->makeVariants($el);
+ }
+ }
+
+ /** @var \Zend_Form_Element $el */
+ foreach ($this->getElements() as $el) {
+ $name = $el->getName();
+ if (substr($name, 0, 4) === 'var_') {
+ $this->makeVariants($el);
+ }
+ }
+
+ $this->setButtons();
+ }
+
+ public function onSuccess()
+ {
+ foreach ($this->getValues() as $key => $value) {
+ $this->setSubmittedMultiValue($key, $value);
+ }
+
+ $modified = $this->storeModifiedObjects();
+ if ($modified === 0) {
+ $msg = $this->translate('No object has been modified');
+ } elseif ($modified === 1) {
+ $msg = $this->translate('One object has been modified');
+ } else {
+ $msg = sprintf(
+ $this->translate('%d objects have been modified'),
+ $modified
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ /**
+ * No default objects behaviour
+ */
+ protected function onRequest()
+ {
+ IcingaObjectFormHook::callOnSetup($this);
+ if ($this->hasBeenSent()) {
+ $this->handlePost();
+ }
+ }
+
+ protected function handlePost()
+ {
+ $this->callOnRequestCallables();
+ if ($this->shouldBeDeleted()) {
+ $this->deleteObjects();
+ }
+ }
+
+ protected function setSubmittedMultiValue($key, $value)
+ {
+ $parts = preg_split('/_/', $key);
+ $objectsSum = array_pop($parts);
+ $valueSum = array_pop($parts);
+ $property = implode('_', $parts);
+
+ if ($value === '') {
+ $value = null;
+ }
+
+ foreach ($this->getVariants($property) as $json => $objects) {
+ if ($valueSum !== sha1($json)) {
+ continue;
+ }
+
+ if ($objectsSum !== sha1(json_encode($objects))) {
+ continue;
+ }
+
+ if (substr($property, 0, 4) === 'var_') {
+ $property = 'vars.' . substr($property, 4);
+ }
+
+ foreach ($this->getObjects($objects) as $object) {
+ $object->$property = $value;
+ }
+ }
+ }
+
+ protected function storeModifiedObjects()
+ {
+ $modified = 0;
+ $store = $this->getDbObjectStore();
+ foreach ($this->objects as $object) {
+ if ($object->hasBeenModified()) {
+ $modified++;
+ $store->store($object);
+ }
+ }
+
+ return $modified;
+ }
+
+ protected function getDisplayGroupForElement(ZfElement $element)
+ {
+ if ($this->elementGroupMap === null) {
+ $this->resolveDisplayGroups();
+ }
+
+ $name = $element->getName();
+ if (array_key_exists($name, $this->elementGroupMap)) {
+ $groupName = $this->elementGroupMap[$name];
+
+ if ($group = $this->getDisplayGroup($groupName)) {
+ return $group;
+ } elseif ($this->relatedForm) {
+ return $this->stealDisplayGroup($groupName, $this->relatedForm);
+ }
+ }
+
+ return null;
+ }
+
+ protected function stealDisplayGroup($name, QuickForm $form)
+ {
+ if ($group = $form->getDisplayGroup($name)) {
+ $group = clone($group);
+ $group->setElements(array());
+ $this->_displayGroups[$name] = $group;
+ $this->_order[$name] = $group->getOrder();
+ $this->_orderUpdated = true;
+
+ return $group;
+ }
+
+ return null;
+ }
+
+ protected function resolveDisplayGroups()
+ {
+ $this->elementGroupMap = array();
+ if ($form = $this->relatedForm) {
+ $this->extractFormDisplayGroups($form);
+ }
+
+ $this->extractFormDisplayGroups($this);
+ }
+
+ protected function extractFormDisplayGroups(QuickForm $form)
+ {
+ /** @var \Zend_Form_DisplayGroup $group */
+ foreach ($form->getDisplayGroups() as $group) {
+ $groupName = $group->getName();
+ foreach ($group->getElements() as $name => $e) {
+ $this->elementGroupMap[$name] = $groupName;
+ }
+ }
+ }
+
+ protected function makeVariants(ZfElement $element)
+ {
+ $key = $element->getName();
+ $this->removeElement($key);
+ $label = $element->getLabel();
+ $group = $this->getDisplayGroupForElement($element);
+ $description = $element->getDescription();
+
+ foreach ($this->getVariants($key) as $json => $objects) {
+ $value = json_decode($json);
+ $checksum = sha1($json) . '_' . sha1(json_encode($objects));
+
+ $v = clone($element);
+ $v->setName($key . '_' . $checksum);
+ $v->setDescription($description . ' ' . $this->descriptionForObjects($objects));
+ $v->setLabel($label . $this->labelCount($objects));
+ $v->setValue($value);
+ if ($group) {
+ $group->addElement($v);
+ }
+ $this->addElement($v);
+ }
+ }
+
+ protected function getVariants($key)
+ {
+ $variants = array();
+ if (substr($key, 0, 4) === 'var_') {
+ $key = 'vars.' . substr($key, 4);
+ }
+
+ foreach ($this->objects as $name => $object) {
+ $value = json_encode($object->$key);
+ if (! array_key_exists($value, $variants)) {
+ $variants[$value] = array();
+ }
+
+ $variants[$value][] = $name;
+ }
+
+ foreach ($variants as & $objects) {
+ natsort($objects);
+ }
+
+ return $variants;
+ }
+
+ protected function descriptionForObjects($list)
+ {
+ return sprintf(
+ $this->translate('Changing this value affects %d object(s): %s'),
+ count($list),
+ implode(', ', $list)
+ );
+ }
+
+ protected function labelCount($list)
+ {
+ return ' (' . count($list) . ')';
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = $this->object()->getConnection();
+ }
+
+ return $this->db;
+ }
+
+ public function getObjects($names = null)
+ {
+ if ($names === null) {
+ return $this->objects;
+ }
+
+ $res = array();
+
+ foreach ($names as $name) {
+ $res[$name] = $this->objects[$name];
+ }
+
+ return $res;
+ }
+
+ protected function deleteObjects()
+ {
+ $msg = sprintf(
+ '%d objects of type "%s" have been removed',
+ count($this->objects),
+ $this->translate($this->object->getShortTableName())
+ );
+
+ $store = $this->getDbObjectStore();
+ foreach ($this->objects as $object) {
+ $store->delete($object);
+ }
+
+ if ($this->listUrl) {
+ $this->setSuccessUrl($this->listUrl);
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaNotificationForm.php b/application/forms/IcingaNotificationForm.php
new file mode 100644
index 0000000..0fca6b8
--- /dev/null
+++ b/application/forms/IcingaNotificationForm.php
@@ -0,0 +1,298 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaNotificationForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Notification Template'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga notification template you are going to create')
+ ));
+ } else {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Notification'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga notification you are going to create')
+ ));
+
+ $this->eventuallyAddNameRestriction(
+ 'director/notification/apply/filter-by-name'
+ );
+ }
+
+ $this->addDisabledElement()
+ ->addImportsElement()
+ ->addUsersElement()
+ ->addUsergroupsElement()
+ ->addIntervalElement()
+ ->addPeriodElement()
+ ->addTimesElements()
+ ->addAssignmentElements()
+ ->addDisabledElement()
+ ->addCommandElements()
+ ->addEventFilterElements()
+ ->addZoneElements()
+ ->groupMainProperties()
+ ->setButtons();
+ }
+
+ protected function addZoneElements()
+ {
+ if (! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addZoneElement();
+ $this->addDisplayGroup(array('zone_id'), 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addAssignmentElements()
+ {
+ if (!$this->object || !$this->object->isApplyRule()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'apply_to', array(
+ 'label' => $this->translate('Apply to'),
+ 'description' => $this->translate(
+ 'Whether this notification should affect hosts or services'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum(
+ array(
+ 'host' => $this->translate('Hosts'),
+ 'service' => $this->translate('Services'),
+ )
+ )
+ ));
+
+ $applyTo = $this->getSentOrObjectValue('apply_to');
+
+ if (! $applyTo) {
+ return $this;
+ }
+
+ $suggestionContext = ucfirst($applyTo) . 'FilterColumns';
+ $this->addAssignFilter([
+ 'required' => true,
+ 'suggestionContext' => $suggestionContext,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addUsersElement()
+ {
+ $users = $this->enumUsers();
+ if (empty($users)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'extensibleSet',
+ 'users',
+ array(
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Users that should be notified by this notifications'
+ ),
+ 'multiOptions' => $this->optionalEnum($users)
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addUsergroupsElement()
+ {
+ $groups = $this->enumUsergroups();
+ if (empty($groups)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'extensibleSet',
+ 'user_groups',
+ array(
+ 'label' => $this->translate('User groups'),
+ 'description' => $this->translate(
+ 'User groups that should be notified by this notifications'
+ ),
+ 'multiOptions' => $this->optionalEnum($groups)
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addIntervalElement()
+ {
+ $this->addElement(
+ 'text',
+ 'notification_interval',
+ array(
+ 'label' => $this->translate('Notification interval'),
+ 'description' => $this->translate(
+ 'The notification interval (in seconds). This interval is'
+ . ' used for active notifications. Defaults to 30 minutes.'
+ . ' If set to 0, re-notifications are disabled.'
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addTimesElements()
+ {
+ $this->addElement(
+ 'text',
+ 'times_begin',
+ array(
+ 'label' => $this->translate('First notification delay'),
+ 'description' => $this->translate(
+ 'Delay unless the first notification should be sent'
+ ) . '. ' . $this->getTimeValueInfo()
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'times_end',
+ array(
+ 'label' => $this->translate('Last notification'),
+ 'description' => $this->translate(
+ 'When the last notification should be sent'
+ ) . '. ' . $this->getTimeValueInfo()
+ )
+ );
+
+ return $this;
+ }
+
+ protected function getTimeValueInfo()
+ {
+ return $this->translate(
+ 'Unit is seconds unless a suffix is given. Supported suffixes include'
+ . ' ms (milliseconds), s (seconds), m (minutes), h (hours) and d (days).'
+ );
+ }
+
+ /**
+ * @return self
+ */
+ protected function addPeriodElement()
+ {
+ $periods = $this->db->enumTimeperiods();
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'select',
+ 'period_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when this'
+ . ' notification should be triggered. Not set by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function addCommandElements()
+ {
+ if (! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'command_id', array(
+ 'label' => $this->translate('Notification command'),
+ 'description' => $this->translate('Check command definition'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumNotificationCommands()),
+ 'class' => 'autosubmit',
+ ));
+
+ return $this;
+ }
+
+ protected function enumUsers()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_user',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+
+ protected function enumUsergroups()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_usergroup',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+}
diff --git a/application/forms/IcingaObjectFieldForm.php b/application/forms/IcingaObjectFieldForm.php
new file mode 100644
index 0000000..537c95e
--- /dev/null
+++ b/application/forms/IcingaObjectFieldForm.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+
+class IcingaObjectFieldForm extends DirectorObjectForm
+{
+ /** @var IcingaObject Please note that $object would conflict with logic in parent class */
+ protected $icingaObject;
+
+ public function setIcingaObject($object)
+ {
+ $this->icingaObject = $object;
+ $this->className = get_class($object) . 'Field';
+ return $this;
+ }
+
+ public function setup()
+ {
+ $object = $this->icingaObject;
+ $type = $object->getShortTableName();
+ $this->addHidden($type . '_id', $object->get('id'));
+
+ $this->addHtmlHint(
+ 'Custom data fields allow you to easily fill custom variables with'
+ . " meaningful data. It's perfectly legal to override inherited fields."
+ . ' You may for example want to allow "network devices" specifying any'
+ . ' string for vars.snmp_community, but restrict "customer routers" to'
+ . ' a specific set, shown as a dropdown.'
+ );
+
+ // TODO: remove assigned ones!
+ $existingFields = $this->db->enumDatafields();
+ $blacklistedVars = array();
+ $suggestedFields = array();
+
+ foreach ($existingFields as $id => $field) {
+ if (preg_match('/ \(([^\)]+)\)$/', $field, $m)) {
+ $blacklistedVars['$' . $m[1] . '$'] = $id;
+ }
+ }
+
+ // TODO: think about imported existing vars without fields
+ // TODO: extract vars from command line (-> dummy)
+ // TODO: do not suggest chosen ones
+ $argumentVars = array();
+ $argumentVarDescriptions = array();
+ if ($object instanceof IcingaCommand) {
+ $command = $object;
+ } elseif ($object->hasProperty('check_command_id')) {
+ $command = $object->getResolvedRelated('check_command');
+ } else {
+ $command = null;
+ }
+
+ if ($command) {
+ foreach ($command->arguments() as $arg) {
+ if ($arg->argument_format === 'string') {
+ $val = $arg->argument_value;
+ // TODO: create var::extractMacros or so
+
+ if (preg_match_all('/(\$[a-z0-9_]+\$)/i', $val, $m, PREG_PATTERN_ORDER)) {
+ foreach ($m[1] as $val) {
+ if (array_key_exists($val, $blacklistedVars)) {
+ $id = $blacklistedVars[$val];
+
+ // Hint: if not set it might already have been
+ // removed in this loop
+ if (array_key_exists($id, $existingFields)) {
+ $suggestedFields[$id] = $existingFields[$id];
+ unset($existingFields[$id]);
+ }
+ } else {
+ $argumentVars[$val] = $val;
+ $argumentVarDescriptions[$val] = $arg->description;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Prepare combined fields array
+ $fields = array();
+ if (! empty($suggestedFields)) {
+ asort($existingFields);
+ $fields[$this->translate('Suggested fields')] = $suggestedFields;
+ }
+
+ if (! empty($argumentVars)) {
+ ksort($argumentVars);
+ $fields[$this->translate('Argument macros')] = $argumentVars;
+ }
+
+ if (! empty($existingFields)) {
+ $fields[$this->translate('Other available fields')] = $existingFields;
+ }
+
+ $this->addElement('select', 'datafield_id', array(
+ 'label' => 'Field',
+ 'required' => true,
+ 'description' => 'Field to assign',
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum($fields)
+ ));
+
+ if (empty($fields)) {
+ // TODO: show message depending on permissions
+ $msg = $this->translate(
+ 'There are no data fields available. Please ask an administrator to create such'
+ );
+
+ $this->getElement('datafield_id')->addError($msg);
+ }
+
+ if (($id = $this->getSentValue('datafield_id')) && ! ctype_digit($id)) {
+ $this->addElement('text', 'caption', array(
+ 'label' => $this->translate('Caption'),
+ 'required' => true,
+ 'ignore' => true,
+ 'value' => trim($id, '$'),
+ 'description' => $this->translate('The caption which should be displayed')
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate('A description about the field'),
+ 'ignore' => true,
+ 'value' => array_key_exists($id, $argumentVarDescriptions) ? $argumentVarDescriptions[$id] : null,
+ 'rows' => '3',
+ ));
+ }
+
+ $this->addElement('select', 'is_required', array(
+ 'label' => $this->translate('Mandatory'),
+ 'description' => $this->translate('Whether this field should be mandatory'),
+ 'required' => true,
+ 'multiOptions' => array(
+ 'n' => $this->translate('Optional'),
+ 'y' => $this->translate('Mandatory'),
+ )
+ ));
+
+ $filterFields = array();
+ $prefix = null;
+ if ($object instanceof IcingaHost) {
+ $prefix = 'host.vars.';
+ } elseif ($object instanceof IcingaService) {
+ $prefix = 'service.vars.';
+ }
+
+ if ($prefix) {
+ $loader = new IcingaObjectFieldLoader($object);
+ $fields = $loader->getFields();
+
+ foreach ($fields as $varName => $field) {
+ $filterFields[$prefix . $field->varname] = $field->caption;
+ }
+
+ $this->addFilterElement('var_filter', array(
+ 'description' => $this->translate(
+ 'You might want to show this field only when certain conditions are met.'
+ . ' Otherwise it will not be available and values eventually set before'
+ . ' will be cleared once stored'
+ ),
+ 'columns' => $filterFields,
+ ));
+
+ $this->addDisplayGroup(array($this->getElement('var_filter')), 'field_filter', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => 30,
+ 'legend' => $this->translate('Show based on filter')
+ ));
+ }
+
+ $this->setButtons();
+ }
+
+ protected function onRequest()
+ {
+ parent::onRequest();
+ if ($this->getSentValue('delete') === $this->translate('Delete')) {
+ $this->object()->delete();
+ $this->setSuccessUrl($this->getSuccessUrl()->without('field_id'));
+ $this->redirectOnSuccess($this->translate('Field has been removed'));
+ }
+ }
+
+ public function onSuccess()
+ {
+ $fieldId = $this->getValue('datafield_id');
+
+ if (! ctype_digit($fieldId)) {
+ $field = DirectorDatafield::create(array(
+ 'varname' => trim($fieldId, '$'),
+ 'caption' => $this->getValue('caption'),
+ 'description' => $this->getValue('description'),
+ 'datatype' => 'Icinga\Module\Director\DataType\DataTypeString',
+ ));
+ $field->store($this->getDb());
+ $this->setElementValue('datafield_id', $field->get('id'));
+ $this->object()->set('datafield_id', $field->get('id'));
+ }
+
+ $this->object()->set('var_filter', $this->getValue('var_filter'));
+ return parent::onSuccess();
+ }
+}
diff --git a/application/forms/IcingaScheduledDowntimeForm.php b/application/forms/IcingaScheduledDowntimeForm.php
new file mode 100644
index 0000000..b126d59
--- /dev/null
+++ b/application/forms/IcingaScheduledDowntimeForm.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaScheduledDowntimeForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Template name'),
+ 'required' => true,
+ ]);
+ } else {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Downtime name'),
+ 'required' => true,
+ ]);
+ }
+
+ if ($this->object()->isApplyRule()) {
+ $this->eventuallyAddNameRestriction('director/scheduled-downtime/apply/filter-by-name');
+ }
+ $this->addImportsElement();
+ $this->addElement('text', 'author', [
+ 'label' => $this->translate('Author'),
+ 'description' => $this->translate(
+ 'This name will show up as the author for ever related downtime'
+ . ' comment'
+ ),
+ 'required' => ! $this->isTemplate()
+ ]);
+ $this->addElement('textarea', 'comment', [
+ 'label' => $this->translate('Comment'),
+ 'description' => $this->translate(
+ 'Every related downtime will show this comment'
+ ),
+ 'required' => ! $this->isTemplate(),
+ 'rows' => 4,
+ ]);
+ $this->addBoolean('fixed', [
+ 'label' => $this->translate('Fixed'),
+ 'description' => $this->translate(
+ 'Whether this downtime is fixed or flexible. If unsure please'
+ . ' check the related documentation:'
+ . ' https://icinga.com/docs/icinga2/latest/doc/08-advanced-topics/#downtimes'
+ ),
+ 'required' => ! $this->isTemplate(),
+ ]);
+ $this->addElement('text', 'duration', [
+ 'label' => $this->translate('Duration'),
+ 'description' => $this->translate(
+ 'How long the downtime lasts. Only has an effect for flexible'
+ . ' (non-fixed) downtimes. Time in seconds, supported suffixes'
+ . ' include ms (milliseconds), s (seconds), m (minutes),'
+ . ' h (hours) and d (days). To express "90 minutes" you might'
+ . ' want to write 1h 30m'
+ )
+ ]);
+ $this->addDisabledElement();
+ $this->addAssignmentElements();
+ $this->setButtons();
+ }
+
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAssignmentElements()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('select', 'apply_to', [
+ 'label' => $this->translate('Apply to'),
+ 'description' => $this->translate(
+ 'Whether this dependency should affect hosts or services'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $this->optionalEnum([
+ 'host' => $this->translate('Hosts'),
+ 'service' => $this->translate('Services'),
+ ])
+ ]);
+
+ $applyTo = $this->getSentOrObjectValue('apply_to');
+
+ if (! $applyTo) {
+ return $this;
+ }
+
+ if ($applyTo === 'host') {
+ $this->addBoolean('with_services', [
+ 'label' => $this->translate('With Services'),
+ 'description' => $this->translate(
+ 'Whether Downtimes should also explicitly be scheduled for'
+ . ' all Services belonging to affected Hosts'
+ )
+ ]);
+ }
+
+ $suggestionContext = ucfirst($applyTo) . 'FilterColumns';
+ $this->addAssignFilter([
+ 'suggestionContext' => $suggestionContext,
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ $this->setSuccessUrl(
+ 'director/scheduled-downtime',
+ $this->object()->getUrlParams()
+ );
+ }
+}
diff --git a/application/forms/IcingaScheduledDowntimeRangeForm.php b/application/forms/IcingaScheduledDowntimeRangeForm.php
new file mode 100644
index 0000000..b5f95d0
--- /dev/null
+++ b/application/forms/IcingaScheduledDowntimeRangeForm.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaScheduledDowntime;
+use Icinga\Module\Director\Objects\IcingaScheduledDowntimeRange;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaScheduledDowntimeRangeForm extends DirectorObjectForm
+{
+ /** @var IcingaScheduledDowntime */
+ private $downtime;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('scheduled_downtime_id', $this->downtime->get('id'));
+ $this->addElement('text', 'range_key', [
+ 'label' => $this->translate('Day(s)'),
+ 'description' => $this->translate(
+ 'Might be monday, tuesday or 2016-01-28 - have a look at the documentation for more examples'
+ ),
+ ]);
+
+ $this->addElement('text', 'range_value', [
+ 'label' => $this->translate('Timeperiods'),
+ 'description' => $this->translate(
+ 'One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00'
+ ),
+ ]);
+
+ $this->setButtons();
+ }
+
+ public function setScheduledDowntime(IcingaScheduledDowntime $downtime)
+ {
+ $this->downtime = $downtime;
+ $this->setDb($downtime->getConnection());
+ return $this;
+ }
+
+ /**
+ * @param IcingaScheduledDowntimeRange $object
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function deleteObject($object)
+ {
+ $key = $object->get('range_key');
+ $downtime = $this->downtime;
+ $downtime->ranges()->remove($key);
+ $downtime->store();
+ $msg = sprintf(
+ $this->translate('Time range "%s" has been removed from %s'),
+ $key,
+ $downtime->getObjectName()
+ );
+
+ $url = $this->getSuccessUrl()->without(
+ ['range', 'range_type']
+ );
+
+ $this->setSuccessUrl($url);
+ $this->redirectOnSuccess($msg);
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function onSuccess()
+ {
+ $object = $this->object();
+ if ($object->hasBeenModified()) {
+ $this->downtime->ranges()->setRange(
+ $this->getValue('range_key'),
+ $this->getValue('range_value')
+ );
+ }
+
+ if ($this->downtime->hasBeenModified()) {
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->setHttpResponseCode(201);
+ }
+
+ $msg = sprintf(
+ $object->hasBeenLoadedFromDb()
+ ? $this->translate('The %s has successfully been stored')
+ : $this->translate('A new %s has successfully been created'),
+ $this->translate($this->getObjectShortClassName())
+ );
+
+ $this->downtime->store($this->db);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+ if ($object instanceof IcingaObject) {
+ $this->setSuccessUrl(
+ 'director/' . strtolower($this->getObjectShortClassName()),
+ $object->getUrlParams()
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaServiceDictionaryMemberForm.php b/application/forms/IcingaServiceDictionaryMemberForm.php
new file mode 100644
index 0000000..90b8f94
--- /dev/null
+++ b/application/forms/IcingaServiceDictionaryMemberForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Module\Director\Objects\IcingaService;
+
+class IcingaServiceDictionaryMemberForm extends DirectorObjectForm
+{
+ /** @var IcingaService */
+ protected $object;
+
+ private $succeeded;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => !$this->object()->isApplyRule(),
+ 'description' => $this->translate(
+ 'Name for the instance you are going to create'
+ )
+ ]);
+ $this->groupMainProperties()->setButtons();
+ }
+
+ protected function isNew()
+ {
+ return $this->object === null;
+ }
+
+ protected function deleteObject($object)
+ {
+ }
+
+ protected function getObjectClassname()
+ {
+ return IcingaService::class;
+ }
+
+ public function succeeded()
+ {
+ return $this->succeeded;
+ }
+
+ public function onSuccess()
+ {
+ $this->succeeded = true;
+ }
+}
diff --git a/application/forms/IcingaServiceForm.php b/application/forms/IcingaServiceForm.php
new file mode 100644
index 0000000..f22f9e6
--- /dev/null
+++ b/application/forms/IcingaServiceForm.php
@@ -0,0 +1,806 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Web\Widget\Hint;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+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;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Web\Table\ObjectsTableHost;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use RuntimeException;
+
+class IcingaServiceForm extends DirectorObjectForm
+{
+ /** @var IcingaHost */
+ private $host;
+
+ /** @var IcingaServiceSet */
+ private $set;
+
+ private $apply;
+
+ /** @var IcingaService */
+ protected $object;
+
+ /** @var IcingaService */
+ private $applyGenerated;
+
+ private $inheritedFrom;
+
+ /** @var bool|null */
+ private $blacklisted;
+
+ public function setApplyGenerated(IcingaService $applyGenerated)
+ {
+ $this->applyGenerated = $applyGenerated;
+
+ return $this;
+ }
+
+ public function setInheritedFrom($hostname)
+ {
+ $this->inheritedFrom = $hostname;
+
+ return $this;
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ if (!$this->isNew() || $this->providesOverrides()) {
+ $this->tryToFetchHost();
+ }
+
+ if ($this->providesOverrides()) {
+ return;
+ }
+
+ if ($this->host && $this->set) {
+ // Probably never reached, as providesOverrides includes this
+ $this->setupOnHostForSet();
+
+ return;
+ }
+
+ if ($this->set !== null) {
+ $this->setupSetRelatedElements();
+ } elseif ($this->host === null) {
+ $this->setupServiceElements();
+ } else {
+ $this->setupHostRelatedElements();
+ }
+ }
+
+ protected function tryToFetchHost()
+ {
+ try {
+ if ($this->host === null) {
+ $this->host = $this->object->getResolvedRelated('host');
+ }
+ } catch (NestingError $nestingError) {
+ // ignore for the form to load
+ }
+ }
+
+ public function providesOverrides()
+ {
+ return $this->applyGenerated
+ || $this->inheritedFrom
+ || ($this->host && $this->set)
+ || ($this->object && $this->object->usesVarOverrides());
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ protected function addFields()
+ {
+ if ($this->providesOverrides() && $this->hasBeenBlacklisted()) {
+ $this->onAddedFields();
+
+ return;
+ } else {
+ parent::addFields();
+ }
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ protected function onAddedFields()
+ {
+ if (! $this->providesOverrides()) {
+ return;
+ }
+ $hasDeleteButton = false;
+ $isBranch = $this->branch && $this->branch->isBranch();
+
+ if ($this->hasBeenBlacklisted()) {
+ $this->addHtml(
+ Hint::warning($this->translate('This Service has been deactivated on this host')),
+ ['name' => 'HINT_blacklisted']
+ );
+ $group = null;
+ if (! $isBranch) {
+ $this->addDeleteButton($this->translate('Reactivate'));
+ $hasDeleteButton = true;
+ }
+ $this->setSubmitLabel(false);
+ } else {
+ $this->addOverrideHint();
+ $group = $this->getDisplayGroup('custom_fields');
+ if (! $group) {
+ foreach ($this->getDisplayGroups() as $groupName => $eventualGroup) {
+ if (preg_match('/^custom_fields:/', $groupName)) {
+ $group = $eventualGroup;
+ break;
+ }
+ }
+ }
+ if ($group) {
+ $elements = $group->getElements();
+ $group->setElements([$this->getElement('inheritance_hint')]);
+ $group->addElements($elements);
+ $this->setSubmitLabel($this->translate('Override vars'));
+ } else {
+ $this->addElementsToGroup(
+ ['inheritance_hint'],
+ 'custom_fields',
+ 20,
+ $this->translate('Hints regarding this service')
+ );
+
+ $this->setSubmitLabel(false);
+ }
+
+ if (! $isBranch) {
+ $this->addDeleteButton($this->translate('Deactivate'));
+ $hasDeleteButton = true;
+ }
+ }
+
+ if (! $this->hasSubmitButton() && $hasDeleteButton) {
+ $this->addDisplayGroup([$this->deleteButtonName], 'buttons', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'DtDdWrapper',
+ ],
+ 'order' => self::GROUP_ORDER_BUTTONS,
+ ]);
+ }
+ }
+
+ /**
+ * @return IcingaHost|null
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * @param IcingaService $service
+ * @return IcingaService
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getFirstParent(IcingaService $service)
+ {
+ /** @var IcingaService[] $objects */
+ $objects = $service->imports()->getObjects();
+ if (empty($objects)) {
+ throw new RuntimeException('Something went wrong, got no parent');
+ }
+ reset($objects);
+
+ return current($objects);
+ }
+
+ /**
+ * @return bool
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function hasBeenBlacklisted()
+ {
+ if (! $this->providesOverrides() || $this->object === null) {
+ return false;
+ }
+
+ if ($this->blacklisted === null) {
+ $host = $this->host;
+ // Safety check, branches
+ $hostId = $host->get('id');
+ $service = $this->getServiceToBeBlacklisted();
+ $serviceId = $service->get('id');
+ if (! $hostId || ! $serviceId) {
+ return false;
+ }
+ $db = $this->db->getDbAdapter();
+ if ($this->providesOverrides()) {
+ $this->blacklisted = 1 === (int)$db->fetchOne(
+ $db->select()->from('icinga_host_service_blacklist', 'COUNT(*)')
+ ->where('host_id = ?', $hostId)
+ ->where('service_id = ?', $serviceId)
+ );
+ } else {
+ $this->blacklisted = false;
+ }
+ }
+
+ return $this->blacklisted;
+ }
+
+ /**
+ * @param $object
+ * @throws IcingaException
+ * @throws ProgrammingError
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function deleteObject($object)
+ {
+ /** @var IcingaService $object */
+ if ($this->providesOverrides()) {
+ if ($this->hasBeenBlacklisted()) {
+ $this->removeFromBlacklist();
+ } else {
+ $this->blacklist();
+ }
+ } else {
+ parent::deleteObject($object);
+ }
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ protected function blacklist()
+ {
+ $host = $this->host;
+ $service = $this->getServiceToBeBlacklisted();
+
+ $db = $this->db->getDbAdapter();
+ $host->unsetOverriddenServiceVars($this->object->getObjectName())->store();
+
+ if ($db->insert('icinga_host_service_blacklist', [
+ 'host_id' => $host->get('id'),
+ 'service_id' => $service->get('id')
+ ])) {
+ $msg = sprintf(
+ $this->translate('%s has been deactivated on %s'),
+ $service->getObjectName(),
+ $host->getObjectName()
+ );
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ /**
+ * @return IcingaService
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getServiceToBeBlacklisted()
+ {
+ if ($this->set) {
+ return $this->object;
+ } else {
+ return $this->getFirstParent($this->object);
+ }
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function removeFromBlacklist()
+ {
+ $host = $this->host;
+ $service = $this->getServiceToBeBlacklisted();
+
+ $db = $this->db->getDbAdapter();
+ $where = implode(' AND ', [
+ $db->quoteInto('host_id = ?', $host->get('id')),
+ $db->quoteInto('service_id = ?', $service->get('id')),
+ ]);
+ if ($db->delete('icinga_host_service_blacklist', $where)) {
+ $msg = sprintf(
+ $this->translate('%s is no longer deactivated on %s'),
+ $service->getObjectName(),
+ $host->getObjectName()
+ );
+ $this->redirectOnSuccess($msg);
+ }
+ }
+
+ /**
+ * @param IcingaService $service
+ * @return $this
+ */
+ public function createApplyRuleFor(IcingaService $service)
+ {
+ $this->apply = $service;
+ $object = $this->object();
+ $object->set('imports', $service->getObjectName());
+ $object->set('object_type', 'apply');
+ $object->set('object_name', $service->getObjectName());
+
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupServiceElements()
+ {
+ if ($this->object) {
+ $objectType = $this->object->get('object_type');
+ } elseif ($this->preferredObjectType) {
+ $objectType = $this->preferredObjectType;
+ } else {
+ $objectType = 'template';
+ }
+ $this->addHidden('object_type', $objectType);
+ $forceCommandElements = $this->hasPermission('director/admin');
+
+ $this->addNameElement()
+ ->addHostObjectElement()
+ ->addImportsElement()
+ ->addChoices('service')
+ ->addGroupsElement()
+ ->addDisabledElement()
+ ->addApplyForElement()
+ ->groupMainProperties()
+ ->addAssignmentElements()
+ ->addCheckCommandElements($forceCommandElements)
+ ->addCheckExecutionElements()
+ ->addExtraInfoElements()
+ ->addAgentAndZoneElements()
+ ->setButtons();
+ }
+
+ /**
+ * @throws IcingaException
+ * @throws ProgrammingError
+ */
+ protected function addOverrideHint()
+ {
+ if ($this->object && $this->object->usesVarOverrides()) {
+ $hint = $this->translate(
+ 'This service has been generated in an automated way, but still'
+ . ' allows you to override the following properties in a safe way.'
+ );
+ } elseif ($apply = $this->applyGenerated) {
+ $hint = Html::sprintf(
+ $this->translate(
+ 'This service has been generated using the %s apply rule, assigned where %s'
+ ),
+ Link::create(
+ $apply->getObjectName(),
+ 'director/service',
+ ['id' => $apply->get('id')],
+ ['data-base-target' => '_next']
+ ),
+ (string) Filter::fromQueryString($apply->assign_filter)
+ );
+ } elseif ($this->host && $this->set) {
+ $hint = Html::sprintf(
+ $this->translate(
+ 'This service belongs to the %s Service Set. Still, you might want'
+ . ' to override the following properties for this host only.'
+ ),
+ Link::create(
+ $this->set->getObjectName(),
+ 'director/serviceset',
+ ['id' => $this->set->get('id')],
+ ['data-base-target' => '_next']
+ )
+ );
+ } elseif ($this->inheritedFrom) {
+ $msg = $this->translate(
+ 'This service has been inherited from %s. Still, you might want'
+ . ' to change the following properties for this host only.'
+ );
+
+ $name = $this->inheritedFrom;
+ $link = Link::create(
+ $name,
+ 'director/service',
+ [
+ 'host' => $name,
+ 'name' => $this->object->getObjectName(),
+ ],
+ ['data-base-target' => '_next']
+ );
+
+ $hint = Html::sprintf($msg, $link);
+ } else {
+ throw new ProgrammingError('Got no override hint for your situation');
+ }
+
+ $this->setSubmitLabel($this->translate('Override vars'));
+
+ $this->addHtmlHint($hint, ['name' => 'inheritance_hint']);
+ }
+
+ protected function setupOnHostForSet()
+ {
+ $msg = $this->translate(
+ 'This service belongs to the service set "%s". Still, you might want'
+ . ' to change the following properties for this host only.'
+ );
+
+ $name = $this->set->getObjectName();
+ $link = Link::create(
+ $name,
+ 'director/serviceset',
+ ['name' => $name],
+ ['data-base-target' => '_next']
+ );
+
+ $this->addHtmlHint(
+ Html::sprintf($msg, $link),
+ ['name' => 'inheritance_hint']
+ );
+
+ $this->addElementsToGroup(
+ ['inheritance_hint'],
+ 'custom_fields',
+ 50,
+ $this->translate('Custom properties')
+ );
+
+ $this->setSubmitLabel($this->translate('Override vars'));
+ }
+
+ protected function addAssignmentElements()
+ {
+ $this->addAssignFilter([
+ 'suggestionContext' => 'HostFilterColumns',
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupHostRelatedElements()
+ {
+ $this->addHidden('host', $this->host->getObjectName());
+ $this->addHidden('object_type', 'object');
+ $this->addImportsElement();
+ $imports = $this->getSentOrObjectValue('imports');
+
+ if ($this->hasBeenSent()) {
+ $imports = $this->getElement('imports')->setValue($imports)->getValue();
+ }
+
+ if ($this->isNew() && empty($imports)) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->addNameElement()
+ ->addChoices('service')
+ ->addDisabledElement()
+ ->addGroupsElement()
+ ->groupMainProperties()
+ ->addCheckCommandElements()
+ ->addExtraInfoElements()
+ ->setButtons();
+
+ $this->setDefaultNameFromTemplate($imports);
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function setupSetRelatedElements()
+ {
+ $this->addHidden('service_set', $this->set->getObjectName());
+ $this->addHidden('object_type', 'apply');
+ $this->addImportsElement();
+ $this->setButtons();
+ $imports = $this->getSentOrObjectValue('imports');
+
+ if ($this->hasBeenSent()) {
+ $imports = $this->getElement('imports')->setValue($imports)->getValue();
+ }
+
+ if ($this->isNew() && empty($imports)) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ $this->addNameElement()
+ ->addDisabledElement()
+ ->addGroupsElement()
+ ->groupMainProperties();
+
+ if ($this->hasPermission('director/admin')) {
+ $this->addCheckCommandElements(true)
+ ->addCheckExecutionElements(true)
+ ->addExtraInfoElements();
+ }
+
+ $this->setDefaultNameFromTemplate($imports);
+ }
+
+ public function setServiceSet(IcingaServiceSet $set)
+ {
+ $this->set = $set;
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addNameElement()
+ {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Name'),
+ 'required' => !$this->object()->isApplyRule(),
+ 'description' => $this->translate(
+ 'Name for the Icinga service you are going to create'
+ )
+ ));
+
+ if ($this->object()->isApplyRule()) {
+ $this->eventuallyAddNameRestriction('director/service/apply/filter-by-name');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addHostObjectElement()
+ {
+ if ($this->isObject()) {
+ $this->addElement('select', 'host', [
+ 'label' => $this->translate('Host'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($this->enumHostsAndTemplates()),
+ 'description' => $this->translate(
+ 'Choose the host this single service should be assigned to'
+ )
+ ]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ 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;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addGroupsElement()
+ {
+ $groups = $this->enumServicegroups();
+
+ if (! empty($groups)) {
+ $this->addElement('extensibleSet', 'groups', array(
+ 'label' => $this->translate('Groups'),
+ 'multiOptions' => $this->optionallyAddFromEnum($groups),
+ 'positional' => false,
+ 'description' => $this->translate(
+ 'Service groups that should be directly assigned to this service.'
+ . ' Servicegroups can be useful for various reasons. They are'
+ . ' helpful to provided service-type specific view in Icinga Web 2,'
+ . ' either for custom dashboards or as an instrument to enforce'
+ . ' restrictions. Service groups can be directly assigned to'
+ . ' single services or to service templates.'
+ )
+ ));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addAgentAndZoneElements()
+ {
+ if (!$this->isTemplate()) {
+ return $this;
+ }
+
+ $this->optionalBoolean(
+ 'use_agent',
+ $this->translate('Run on agent'),
+ $this->translate(
+ 'Whether the check commmand for this service should be executed'
+ . ' on the Icinga agent'
+ )
+ );
+ $this->addZoneElement();
+
+ $elements = array(
+ 'use_agent',
+ 'zone_id',
+ );
+ $this->addDisplayGroup($elements, 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Icinga Agent and zone settings')
+ ));
+
+ return $this;
+ }
+
+ protected function enumHostsAndTemplates()
+ {
+ if ($this->branch && $this->branch->isBranch()) {
+ return $this->enumHosts();
+ }
+
+ return [
+ $this->translate('Templates') => $this->enumHostTemplates(),
+ $this->translate('Hosts') => $this->enumHosts(),
+ ];
+ }
+
+ protected function enumHostTemplates()
+ {
+ $names = array_values($this->db->enumHostTemplates());
+ return array_combine($names, $names);
+ }
+
+ protected function enumHosts()
+ {
+ $db = $this->db->getDbAdapter();
+ $table = new ObjectsTableHost($this->db);
+ $table->setAuth($this->getAuth());
+ if ($this->branch && $this->branch->isBranch()) {
+ $table->setBranchUuid($this->branch->getUuid());
+ }
+ $result = [];
+ foreach ($db->fetchAll($table->getQuery()->reset(\Zend_Db_Select::LIMIT_COUNT)) as $row) {
+ $result[$row->object_name] = $row->object_name;
+ }
+
+ return $result;
+ }
+
+ protected function enumServicegroups()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_servicegroup',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+
+ protected function succeedForOverrides()
+ {
+ $vars = array();
+ foreach ($this->object->vars() as $key => $var) {
+ $vars[$key] = $var->getValue();
+ }
+
+ $host = $this->host;
+ $serviceName = $this->object->getObjectName();
+
+ $this->host->overrideServiceVars($serviceName, (object) $vars);
+
+ if ($host->hasBeenModified()) {
+ $msg = sprintf(
+ empty($vars)
+ ? $this->translate('All overrides have been removed from "%s"')
+ : $this->translate('The given properties have been stored for "%s"'),
+ $this->translate($host->getObjectName())
+ );
+
+ $this->getDbObjectStore()->store($host);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->providesOverrides()) {
+ $this->succeedForOverrides();
+ return;
+ }
+
+ parent::onSuccess();
+ }
+
+ /**
+ * @param array $imports
+ */
+ protected function setDefaultNameFromTemplate($imports)
+ {
+ if ($this->hasBeenSent()) {
+ $name = $this->getSentOrObjectValue('object_name');
+ if ($name === null || !strlen($name)) {
+ $this->setElementValue('object_name', end($imports));
+ $this->object->set('object_name', end($imports));
+ }
+ }
+ }
+}
diff --git a/application/forms/IcingaServiceGroupForm.php b/application/forms/IcingaServiceGroupForm.php
new file mode 100644
index 0000000..db23cbb
--- /dev/null
+++ b/application/forms/IcingaServiceGroupForm.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaServiceGroupForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Servicegroup'),
+ 'required' => true,
+ 'description' => $this->translate('Icinga object name for this service group')
+ ]);
+
+ $this->addGroupDisplayNameElement()
+ ->addAssignmentElements()
+ ->setButtons();
+ }
+
+ protected function addAssignmentElements()
+ {
+ $this->addAssignFilter([
+ 'suggestionContext' => 'ServiceFilterColumns',
+ 'required' => false,
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. The'
+ . ' "contains" operator is valid for arrays only. Please use'
+ . ' wildcards and the = (equals) operator when searching for'
+ . ' partial string matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaServiceSetForm.php b/application/forms/IcingaServiceSetForm.php
new file mode 100644
index 0000000..21508d5
--- /dev/null
+++ b/application/forms/IcingaServiceSetForm.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaServiceSetForm extends DirectorObjectForm
+{
+ protected $host;
+
+ protected $listUrl = 'director/services/sets';
+
+ public function setup()
+ {
+ if ($this->host === null) {
+ $this->setupTemplate();
+ } else {
+ $this->setupHost();
+ }
+
+ $this->setButtons();
+ }
+
+ protected function setupTemplate()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Service set name'),
+ 'description' => $this->translate(
+ 'A short name identifying this set of services'
+ ),
+ 'required' => true,
+ ])
+ ->eventuallyAddNameRestriction('director/service_set/filter-by-name')
+ ->addHidden('object_type', 'template')
+ ->addDescriptionElement()
+ ->addAssignmentElements();
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ if ($this->host) {
+ $this->setSuccessUrl(
+ 'director/host/services',
+ array('name' => $this->host->getObjectName())
+ );
+ } else {
+ parent::setObjectSuccessUrl();
+ }
+ }
+
+ protected function setupHost()
+ {
+ $object = $this->object();
+ if ($this->hasBeenSent()) {
+ $object->set('object_name', $this->getSentValue('imports'));
+ $object->set('imports', $object->object_name);
+ }
+
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->addSingleImportsElement();
+ }
+
+ if (count($object->get('imports'))) {
+ $description = $object->getResolvedProperty('description');
+ if ($description) {
+ $this->addHtmlHint($description);
+ }
+ }
+
+ $this->addHidden('object_type', 'object');
+ $this->addHidden('host', $this->host->getObjectName());
+ $this->groupMainProperties();
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ protected function addSingleImportsElement()
+ {
+ $enum = $this->enumAllowedTemplates();
+
+ $this->addElement('select', 'imports', array(
+ 'label' => $this->translate('Service set'),
+ 'description' => $this->translate(
+ 'The service set that should be assigned to this host'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionallyAddFromEnum($enum),
+ 'class' => 'autosubmit'
+ ));
+
+ return $this;
+ }
+
+ protected function addDescriptionElement()
+ {
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'A meaningful description explaining your users what to expect'
+ . ' when assigning this set of services'
+ ),
+ 'rows' => '3',
+ 'required' => ! $this->isTemplate(),
+ ));
+
+ return $this;
+ }
+
+ protected function addAssignmentElements()
+ {
+ if (! $this->hasPermission('director/service_set/apply')) {
+ return $this;
+ }
+
+ $this->addAssignFilter([
+ 'suggestionContext' => 'HostFilterColumns',
+ 'description' => $this->translate(
+ 'This allows you to configure an assignment filter. Please feel'
+ . ' free to combine as many nested operators as you want. You'
+ . ' might also want to skip this, define it later and/or just'
+ . ' add this set of services to single hosts. The "contains"'
+ . ' operator is valid for arrays only. Please use wildcards and'
+ . ' the = (equals) operator when searching for partial string'
+ . ' matches, like in *.example.com'
+ )
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaServiceVarForm.php b/application/forms/IcingaServiceVarForm.php
new file mode 100644
index 0000000..e7ac4a0
--- /dev/null
+++ b/application/forms/IcingaServiceVarForm.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+/**
+ * @deprecated
+ */
+class IcingaServiceVarForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('select', 'service_id', array(
+ 'label' => $this->translate('Service'),
+ 'description' => $this->translate('The name of the service'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumServices()),
+ 'required' => true
+ ));
+
+ $this->addElement('text', 'varname', array(
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate('service var name')
+ ));
+
+ $this->addElement('textarea', 'varvalue', array(
+ 'label' => $this->translate('Value'),
+ 'description' => $this->translate('service var value')
+ ));
+
+ $this->addElement('text', 'format', array(
+ 'label' => $this->translate('Format'),
+ 'description' => $this->translate('value format')
+ ));
+ }
+}
diff --git a/application/forms/IcingaTemplateChoiceForm.php b/application/forms/IcingaTemplateChoiceForm.php
new file mode 100644
index 0000000..31fe610
--- /dev/null
+++ b/application/forms/IcingaTemplateChoiceForm.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\IcingaTemplateChoice;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaTemplateChoiceForm extends DirectorObjectForm
+{
+ private $choiceType;
+
+ public static function create($type, Db $db)
+ {
+ return static::load()->setDb($db)->setChoiceType($type);
+ }
+
+ public function optionallyLoad($name)
+ {
+ if ($name !== null) {
+ /** @var IcingaTemplateChoice $class - cheating IDE */
+ $class = $this->getObjectClassName();
+ $this->setObject($class::load($name, $this->getDb()));
+ }
+
+ return $this;
+ }
+
+ protected function getObjectClassname()
+ {
+ if ($this->className === null) {
+ return 'Icinga\\Module\\Director\\Objects\\IcingaTemplateChoice'
+ . ucfirst($this->choiceType);
+ }
+
+ return $this->className;
+ }
+
+ public function setChoiceType($type)
+ {
+ $this->choiceType = $type;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Choice name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This will be shown as a label for the given choice'
+ )
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'rows' => 4,
+ 'description' => $this->translate(
+ 'A detailled description explaining what this choice is all about'
+ )
+ ));
+
+ $this->addElement('extensibleSet', 'members', array(
+ 'label' => $this->translate('Available choices'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Your users will be allowed to choose among those templates'
+ ),
+ 'multiOptions' => $this->fetchUnboundTemplates()
+ ));
+
+ $this->addElement('text', 'min_required', array(
+ 'label' => $this->translate('Minimum required'),
+ 'description' => $this->translate(
+ 'Choosing this many options will be mandatory for this Choice.'
+ . ' Setting this to zero will leave this Choice optional, setting'
+ . ' it to one results in a "required" Choice. You can use higher'
+ . ' numbers to enforce multiple options, this Choice will then turn'
+ . ' into a multi-selection element.'
+ ),
+ 'value' => 0,
+ ));
+
+ $this->addElement('text', 'max_allowed', array(
+ 'label' => $this->translate('Allowed maximum'),
+ 'description' => $this->translate(
+ 'It will not be allowed to choose more than this many options.'
+ . ' Setting it to one (1) will result in a drop-down box, a'
+ . ' higher number will turn this into a multi-selection element.'
+ ),
+ 'value' => 1,
+ ));
+
+ $this->addElement('select', 'required_template', [
+ 'label' => $this->translate('Associated Template'),
+ 'description' => $this->translate(
+ 'Choose Choice Associated Template'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->fetchUnboundTemplates(),
+ ]);
+
+ $this->setButtons();
+ }
+
+ protected function fetchUnboundTemplates()
+ {
+ /** @var IcingaTemplateChoice $object */
+ $object = $this->object();
+ $db = $this->getDb()->getDbAdapter();
+ $table = $object->getObjectTableName();
+ $query = $db->select()->from(
+ ['o' => $table],
+ [
+ 'k' => 'o.object_name',
+ 'v' => 'o.object_name',
+ ]
+ )->where("o.object_type = 'template'");
+ if ($object->hasBeenLoadedFromDb()) {
+ $query->where(
+ 'o.template_choice_id IS NULL OR o.template_choice_id = ?',
+ $object->get('id')
+ );
+ } else {
+ $query->where('o.template_choice_id IS NULL');
+ }
+
+ return $db->fetchPairs($query);
+ }
+
+ protected function setObjectSuccessUrl()
+ {
+ /** @var IcingaTemplateChoice $object */
+ $object = $this->object();
+ $this->setSuccessUrl(
+ 'director/templatechoice/' . $object->getObjectshortTableName(),
+ $object->getUrlParams()
+ );
+ }
+}
diff --git a/application/forms/IcingaTimePeriodForm.php b/application/forms/IcingaTimePeriodForm.php
new file mode 100644
index 0000000..8afcdf3
--- /dev/null
+++ b/application/forms/IcingaTimePeriodForm.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaTimePeriodForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addElement('text', 'object_name', [
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ ]);
+
+ $this->addElement('text', 'display_name', [
+ 'label' => $this->translate('Display Name'),
+ ]);
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'update_method', [
+ 'label' => $this->translate('Update Method'),
+ 'value' => 'LegacyTimePeriod',
+ ]);
+ } else {
+ // TODO: I'd like to skip this for objects inheriting from a template
+ // with a defined update_method. However, unfortunately it's too
+ // early for $this->object()->getResolvedProperty('update_method').
+ // Should be fixed.
+ $this->addHidden('update_method', 'LegacyTimePeriod');
+ }
+
+ $this->addIncludeExclude()
+ ->addImportsElement()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addIncludeExclude()
+ {
+ $periods = [];
+ foreach ($this->db->enumTimeperiods() as $id => $period) {
+ if ($this->object === null || $this->object->get('object_name') !== $period) {
+ $periods[$period] = $period;
+ }
+ }
+
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement('extensibleSet', 'includes', [
+ 'label' => $this->translate('Include period'),
+ 'multiOptions' => $this->optionalEnum($periods),
+ 'description' => $this->translate(
+ 'Include other time periods into this.'
+ ),
+ ]);
+
+ $this->addElement('extensibleSet', 'excludes', [
+ 'label' => $this->translate('Exclude period'),
+ 'multiOptions' => $this->optionalEnum($periods),
+ 'description' => $this->translate(
+ 'Exclude other time periods from this.'
+ ),
+ ]);
+
+ $this->optionalBoolean(
+ 'prefer_includes',
+ $this->translate('Prefer includes'),
+ $this->translate('Whether to prefer timeperiods includes or excludes. Default to true.')
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaTimePeriodRangeForm.php b/application/forms/IcingaTimePeriodRangeForm.php
new file mode 100644
index 0000000..977684e
--- /dev/null
+++ b/application/forms/IcingaTimePeriodRangeForm.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Objects\IcingaTimePeriodRange;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaTimePeriodRangeForm extends DirectorObjectForm
+{
+ /**
+ * @var IcingaTimePeriod
+ */
+ private $period;
+
+ public function setup()
+ {
+ $this->addHidden('timeperiod_id', $this->period->get('id'));
+ $this->addElement('text', 'range_key', array(
+ 'label' => $this->translate('Day(s)'),
+ 'description' => $this->translate(
+ 'Might be monday, tuesday or 2016-01-28 - have a look at the documentation for more examples'
+ ),
+ ));
+
+ $this->addElement('text', 'range_value', array(
+ 'label' => $this->translate('Timerperiods'),
+ 'description' => $this->translate(
+ 'One or more time periods, e.g. 00:00-24:00 or 00:00-09:00,17:00-24:00'
+ ),
+ ));
+
+ $this->setButtons();
+ }
+
+ public function setTimePeriod(IcingaTimePeriod $period)
+ {
+ $this->period = $period;
+ $this->setDb($period->getConnection());
+ return $this;
+ }
+
+ /**
+ * @param IcingaTimePeriodRange $object
+ */
+ protected function deleteObject($object)
+ {
+ $key = $object->get('range_key');
+ $period = $this->period;
+ $period->ranges()->remove($key);
+ $period->store();
+ $msg = sprintf(
+ 'Time period range "%s" has been removed from %s',
+ $key,
+ $period->getObjectName()
+ );
+
+ $url = $this->getSuccessUrl()->without(
+ ['range', 'range_type']
+ );
+
+ $this->setSuccessUrl($url);
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object();
+ if ($object->hasBeenModified()) {
+ $this->period->ranges()->setRange(
+ $this->getValue('range_key'),
+ $this->getValue('range_value')
+ );
+ }
+
+ if ($this->period->hasBeenModified()) {
+ if (! $object->hasBeenLoadedFromDb()) {
+ $this->setHttpResponseCode(201);
+ }
+
+ $msg = sprintf(
+ $object->hasBeenLoadedFromDb()
+ ? $this->translate('The %s has successfully been stored')
+ : $this->translate('A new %s has successfully been created'),
+ $this->translate($this->getObjectShortClassName())
+ );
+
+ $this->period->store($this->db);
+ } else {
+ if ($this->isApiRequest()) {
+ $this->setHttpResponseCode(304);
+ }
+ $msg = $this->translate('No action taken, object has not been modified');
+ }
+ if ($object instanceof IcingaObject) {
+ $this->setSuccessUrl(
+ 'director/' . strtolower($this->getObjectShortClassName()),
+ $object->getUrlParams()
+ );
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+}
diff --git a/application/forms/IcingaUserForm.php b/application/forms/IcingaUserForm.php
new file mode 100644
index 0000000..bff2252
--- /dev/null
+++ b/application/forms/IcingaUserForm.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaUserForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addObjectTypeElement();
+ if (! $this->hasObjectType()) {
+ $this->groupMainProperties();
+ return;
+ }
+
+ if ($this->isTemplate()) {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('User template name'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga user template you are going to create')
+ ));
+ } else {
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Username'),
+ 'required' => true,
+ 'description' => $this->translate('Name for the Icinga user object you are going to create')
+ ));
+ }
+
+ if (! $this->isTemplate()) {
+ $this->addElement('text', 'email', array(
+ 'label' => $this->translate('Email'),
+ 'description' => $this->translate('The Email address of the user.')
+ ));
+
+ $this->addElement('text', 'pager', array(
+ 'label' => $this->translate('Pager'),
+ 'description' => $this->translate('The pager address of the user.')
+ ));
+ }
+
+ $this->addGroupsElement()
+ ->addImportsElement()
+ ->addDisplayNameElement()
+ ->addEnableNotificationsElement()
+ ->addDisabledElement()
+ ->addZoneElements()
+ ->addPeriodElement()
+ ->addEventFilterElements()
+ ->groupMainProperties()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addZoneElements()
+ {
+ if (! $this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addZoneElement();
+ $this->addDisplayGroup(array('zone_id'), 'clustering', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function addEnableNotificationsElement()
+ {
+ $this->optionalBoolean(
+ 'enable_notifications',
+ $this->translate('Send notifications'),
+ $this->translate('Whether to send notifications for this user')
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addGroupsElement()
+ {
+ $groups = $this->enumUsergroups();
+
+ if (empty($groups)) {
+ return $this;
+ }
+
+ $this->addElement('extensibleSet', 'groups', array(
+ 'label' => $this->translate('Groups'),
+ 'multiOptions' => $this->optionallyAddFromEnum($groups),
+ 'positional' => false,
+ 'description' => $this->translate(
+ 'User groups that should be directly assigned to this user. Groups can be useful'
+ . ' for various reasons. You might prefer to send notifications to groups instead of'
+ . ' single users'
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addDisplayNameElement()
+ {
+ if ($this->isTemplate()) {
+ return $this;
+ }
+
+ $this->addElement('text', 'display_name', array(
+ 'label' => $this->translate('Display name'),
+ 'description' => $this->translate(
+ 'Alternative name for this user. In case your object name is a'
+ . ' username, this could be the full name of the corresponding person'
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addPeriodElement()
+ {
+ $periods = $this->db->enumTimeperiods();
+ if (empty($periods)) {
+ return $this;
+ }
+
+ $this->addElement(
+ 'select',
+ 'period_id',
+ array(
+ 'label' => $this->translate('Time period'),
+ 'description' => $this->translate(
+ 'The name of a time period which determines when notifications'
+ . ' to this User should be triggered. Not set by default.'
+ ),
+ 'multiOptions' => $this->optionalEnum($periods),
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function groupObjectDefinition()
+ {
+ $elements = array(
+ 'object_type',
+ 'object_name',
+ 'display_name',
+ 'imports',
+ 'groups',
+ 'email',
+ 'pager',
+ 'period_id',
+ 'enable_notifications',
+ 'disabled',
+ );
+ $this->addDisplayGroup($elements, 'object_definition', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => self::GROUP_ORDER_OBJECT_DEFINITION,
+ 'legend' => $this->translate('User properties')
+ ));
+ }
+
+ /**
+ * @return array
+ */
+ protected function enumUsergroups()
+ {
+ $db = $this->db->getDbAdapter();
+ $select = $db->select()->from(
+ 'icinga_usergroup',
+ array(
+ 'name' => 'object_name',
+ 'display' => 'COALESCE(display_name, object_name)'
+ )
+ )->where('object_type = ?', 'object')->order('display');
+
+ return $db->fetchPairs($select);
+ }
+}
diff --git a/application/forms/IcingaUserGroupForm.php b/application/forms/IcingaUserGroupForm.php
new file mode 100644
index 0000000..d9706b4
--- /dev/null
+++ b/application/forms/IcingaUserGroupForm.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaUserGroupForm extends DirectorObjectForm
+{
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Usergroup'),
+ 'required' => true,
+ 'description' => $this->translate('Icinga object name for this user group')
+ ));
+
+ $this->addGroupDisplayNameElement()
+ ->addZoneElements()
+ ->groupMainProperties()
+ ->setButtons();
+ }
+
+ /**
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addZoneElements()
+ {
+ $this->addZoneElement(true);
+ $this->addDisplayGroup(['zone_id'], 'clustering', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'dl']],
+ 'Fieldset',
+ ],
+ 'order' => self::GROUP_ORDER_CLUSTERING,
+ 'legend' => $this->translate('Zone settings')
+ ]);
+
+ return $this;
+ }
+}
diff --git a/application/forms/IcingaZoneForm.php b/application/forms/IcingaZoneForm.php
new file mode 100644
index 0000000..bf27cae
--- /dev/null
+++ b/application/forms/IcingaZoneForm.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class IcingaZoneForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addHidden('object_type', 'object');
+
+ $this->addElement('text', 'object_name', array(
+ 'label' => $this->translate('Zone name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Name for the Icinga zone you are going to create'
+ )
+ ));
+
+ $this->addElement('select', 'is_global', array(
+ 'label' => $this->translate('Global zone'),
+ 'description' => $this->translate(
+ 'Whether this zone should be available everywhere. Please note that'
+ . ' it rarely leads to the desired result when you try to distribute'
+ . ' global zones in distrubuted environments'
+ ),
+ 'multiOptions' => array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('select', 'parent_id', array(
+ 'label' => $this->translate('Parent Zone'),
+ 'description' => $this->translate('Chose an (optional) parent zone'),
+ 'multiOptions' => $this->optionalEnum($this->db->enumZones()),
+ ));
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/ImportCheckForm.php b/application/forms/ImportCheckForm.php
new file mode 100644
index 0000000..31c9781
--- /dev/null
+++ b/application/forms/ImportCheckForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class ImportCheckForm extends DirectorForm
+{
+ /** @var ImportSource */
+ protected $source;
+
+ public function setImportSource(ImportSource $source)
+ {
+ $this->source = $source;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->submitLabel = false;
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Check for changes'),
+ 'decorators' => ['ViewHelper']
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ $source = $this->source;
+ if ($source->checkForChanges()) {
+ $this->setSuccessMessage(
+ $this->translate('This Import Source provides modified data')
+ );
+ } else {
+ $this->setSuccessMessage(
+ $this->translate(
+ 'Nothing to do, data provided by this Import Source'
+ . " didn't change since the last import run"
+ )
+ );
+ }
+
+ if ($source->get('import_state') === 'failing') {
+ $this->addError($this->translate('Checking this Import Source failed'));
+ } else {
+ parent::onSuccess();
+ }
+ }
+}
diff --git a/application/forms/ImportRowModifierForm.php b/application/forms/ImportRowModifierForm.php
new file mode 100644
index 0000000..9e53bd9
--- /dev/null
+++ b/application/forms/ImportRowModifierForm.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Hook\PropertyModifierHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use RuntimeException;
+
+class ImportRowModifierForm extends DirectorObjectForm
+{
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var ImportSourceHook */
+ protected $importSource;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('source_id', $this->source->id);
+
+ $this->addElement('text', 'property_name', array(
+ 'label' => $this->translate('Property'),
+ 'description' => $this->translate(
+ 'Please start typing for a list of suggestions. Dots allow you to access nested'
+ . ' properties: column.some.key. Such nested properties cannot be modified in-place,'
+ . ' but you can store the modified value to a new "target property"'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit director-suggest',
+ 'data-suggestion-context' => 'importsourceproperties!' . $this->source->id,
+ ));
+
+ $this->addElement('text', 'target_property', [
+ 'label' => $this->translate('Target property'),
+ 'description' => $this->translate(
+ 'You might want to write the modified value to another (new) property.'
+ . ' This property name can be defined here, the original property would'
+ . ' remain unmodified. Please leave this blank in case you just want to'
+ . ' modify the value of a specific property'
+ ),
+ ]);
+
+ $this->addElement('textarea', 'description', [
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this Import Row Modifier. This should explain'
+ . " it's purpose and why it has been put in place at all."
+ ),
+ 'rows' => '3',
+ ]);
+
+ $error = false;
+ try {
+ $mods = $this->enumModifiers();
+ } catch (Exception $e) {
+ $error = $e->getMessage();
+ $mods = $this->optionalEnum([]);
+ }
+
+ $this->addElement('select', 'provider_class', [
+ 'label' => $this->translate('Modifier'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'A property modifier allows you to modify a specific property at import time'
+ ),
+ 'multiOptions' => $this->optionalEnum($mods),
+ 'class' => 'autosubmit',
+ ]);
+ if ($error) {
+ $this->getElement('provider_class')->addError($error);
+ }
+
+ try {
+ if ($class = $this->getSentValue('provider_class')) {
+ if ($class && array_key_exists($class, $mods)) {
+ $this->addSettings($class);
+ }
+ } elseif ($class = $this->object()->get('provider_class')) {
+ $this->addSettings($class);
+ }
+
+ // TODO: next line looks like obsolete duplicate code to me
+ $this->addSettings();
+ } catch (Exception $e) {
+ $this->getElement('provider_class')->addError($e->getMessage());
+ }
+
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+
+ $this->setButtons();
+ }
+
+ public function getSetting($name, $default = null)
+ {
+ if ($this->hasBeenSent()) {
+ $value = $this->getSentValue($name);
+ if ($value !== null) {
+ return $value;
+ }
+ }
+ if ($this->isNew()) {
+ $value = $this->getElement($name)->getValue();
+ if ($value === null) {
+ return $default;
+ }
+
+ return $value;
+ }
+
+ return $this->object()->getSetting($name, $default);
+ }
+
+ /**
+ * @return ImportSourceHook
+ * @throws ConfigurationError
+ */
+ protected function getImportSource()
+ {
+ if ($this->importSource === null) {
+ $this->importSource = ImportSourceHook::loadByName(
+ $this->source->get('source_name'),
+ $this->db
+ );
+ }
+
+ return $this->importSource;
+ }
+
+ protected function enumModifiers()
+ {
+ /** @var PropertyModifierHook[] $hooks */
+ $hooks = Hook::all('Director\\PropertyModifier');
+ $enum = [];
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+
+ asort($enum);
+
+ return $enum;
+ }
+
+ /**
+ * @param null $class
+ */
+ protected function addSettings($class = null)
+ {
+ if ($class === null) {
+ $class = $this->getValue('provider_class');
+ }
+
+ if ($class !== null) {
+ if (! class_exists($class)) {
+ throw new RuntimeException(sprintf(
+ 'The hooked class "%s" for this property modifier does no longer exist',
+ $class
+ ));
+ }
+
+ $class::addSettingsFormFields($this);
+ }
+ }
+
+ public function setSource(ImportSource $source)
+ {
+ $this->source = $source;
+
+ return $this;
+ }
+}
diff --git a/application/forms/ImportRunForm.php b/application/forms/ImportRunForm.php
new file mode 100644
index 0000000..9f6494f
--- /dev/null
+++ b/application/forms/ImportRunForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class ImportRunForm extends DirectorForm
+{
+ /** @var ImportSource */
+ protected $source;
+
+ public function setImportSource(ImportSource $source)
+ {
+ $this->source = $source;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->submitLabel = false;
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Trigger Import Run'),
+ 'decorators' => ['ViewHelper']
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ $source = $this->source;
+ if ($source->runImport()) {
+ $this->setSuccessMessage(
+ $this->translate('Imported new data from this Import Source')
+ );
+ } else {
+ $this->setSuccessMessage(
+ $this->translate(
+ 'Nothing to do, data provided by this Import Source'
+ . " didn't change since the last import run"
+ )
+ );
+ }
+
+ if ($source->get('import_state') === 'failing') {
+ $this->addError($this->translate('Triggering this Import Source failed'));
+ } else {
+ parent::onSuccess();
+ }
+ }
+}
diff --git a/application/forms/ImportSourceForm.php b/application/forms/ImportSourceForm.php
new file mode 100644
index 0000000..b547a32
--- /dev/null
+++ b/application/forms/ImportSourceForm.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+use Icinga\Web\Hook;
+
+class ImportSourceForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $this->addElement('text', 'source_name', array(
+ 'label' => $this->translate('Import source name'),
+ 'description' => $this->translate(
+ 'A short name identifying this import source. Use something meaningful,'
+ . ' like "Hosts from Puppet", "Users from Active Directory" or similar'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('textarea', 'description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this Import Source. This should explain'
+ . " what kind of data you're going to import from this source."
+ ),
+ 'rows' => '3',
+ ));
+
+ $this->addElement('select', 'provider_class', array(
+ 'label' => $this->translate('Source Type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($this->enumSourceTypes()),
+ 'description' => $this->translate(
+ 'These are different data providers fetching data from various sources.'
+ . ' You didn\'t find what you\'re looking for? Import sources are implemented'
+ . ' as a hook in Director, so you might find (or write your own) Icinga Web 2'
+ . ' module fetching data from wherever you want'
+ ),
+ 'class' => 'autosubmit'
+ ));
+
+ $this->addSettings();
+ $this->setButtons();
+ }
+
+ public function getSentOrObjectSetting($name, $default = null)
+ {
+ if ($this->hasObject()) {
+ $value = $this->getSentValue($name);
+ if ($value === null) {
+ /** @var ImportSource $object */
+ $object = $this->getObject();
+
+ return $object->getSetting($name, $default);
+ } else {
+ return $value;
+ }
+ } else {
+ return $this->getSentValue($name, $default);
+ }
+ }
+
+ public function hasChangedSetting($name)
+ {
+ if ($this->hasBeenSent() && $this->hasObject()) {
+ /** @var ImportSource $object */
+ $object = $this->getObject();
+ return $object->getStoredSetting($name)
+ !== $this->getSentValue($name);
+ } else {
+ return false;
+ }
+ }
+
+ protected function addSettings()
+ {
+ if (! ($class = $this->getProviderClass())) {
+ return;
+ }
+
+ $defaultKeyCol = $this->getDefaultKeyColumnName();
+
+ $this->addElement('text', 'key_column', array(
+ 'label' => $this->translate('Key column name'),
+ 'description' => $this->translate(
+ 'This must be a column containing unique values like hostnames. Unless otherwise'
+ . ' specified this will then be used as the object_name for the syncronized'
+ . ' Icinga object. Especially when getting started with director please make'
+ . ' sure to strictly follow this rule. Duplicate values for this column on different'
+ . ' rows will trigger a failure, your import run will not succeed. Please pay attention'
+ . ' when synching services, as "purge" will only work correctly with a key_column'
+ . ' corresponding to host!name. Check the "Combine" property modifier in case your'
+ . ' data source cannot provide such a field'
+ ),
+ 'placeholder' => $defaultKeyCol,
+ 'required' => $defaultKeyCol === null,
+ ));
+
+ if (array_key_exists($class, $this->enumSourceTypes())) {
+ $class::addSettingsFormFields($this);
+ foreach ($this->object()->getSettings() as $key => $val) {
+ if ($el = $this->getElement($key)) {
+ $el->setValue($val);
+ }
+ }
+ }
+ }
+
+ protected function getDefaultKeyColumnName()
+ {
+ if (! ($class = $this->getProviderClass())) {
+ return null;
+ }
+
+ if (! class_exists($class)) {
+ return null;
+ }
+
+ return $class::getDefaultKeyColumnName();
+ }
+
+ protected function getProviderClass()
+ {
+ if ($this->hasBeenSent()) {
+ $class = $this->getRequest()->getPost('provider_class');
+ } else {
+ if (! ($class = $this->object()->get('provider_class'))) {
+ return null;
+ }
+ }
+
+ return $class;
+ }
+
+ public function onSuccess()
+ {
+ if (! $this->getValue('key_column')) {
+ if ($default = $this->getDefaultKeyColumnName()) {
+ $this->setElementValue('key_column', $default);
+ $this->object()->set('key_column', $default);
+ }
+ }
+
+ parent::onSuccess();
+ }
+
+ protected function enumSourceTypes()
+ {
+ /** @var ImportSourceHook[] $hooks */
+ $hooks = Hook::all('Director\\ImportSource');
+
+ $enum = array();
+ foreach ($hooks as $hook) {
+ $enum[get_class($hook)] = $hook->getName();
+ }
+ asort($enum);
+
+ return $enum;
+ }
+}
diff --git a/application/forms/KickstartForm.php b/application/forms/KickstartForm.php
new file mode 100644
index 0000000..0079cfb
--- /dev/null
+++ b/application/forms/KickstartForm.php
@@ -0,0 +1,482 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Data\ResourceFactory;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Migrations;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\KickstartHelper;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+
+class KickstartForm extends DirectorForm
+{
+ private $config;
+
+ private $storeConfigLabel;
+
+ private $createDbLabel;
+
+ private $migrateDbLabel;
+
+ /** @var IcingaEndpoint */
+ private $endpoint;
+
+ private $dbResourceName;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->storeConfigLabel = $this->translate('Store configuration');
+ $this->createDbLabel = $this->translate('Create database schema');
+ $this->migrateDbLabel = $this->translate('Apply schema migrations');
+
+ if ($this->dbResourceName === null) {
+ $this->addResourceConfigElements();
+ $this->addResourceDisplayGroup();
+
+ if (!$this->config()->get('db', 'resource')
+ || ($this->config()->get('db', 'resource') !== $this->getResourceName())) {
+ return;
+ }
+ }
+
+ if (!$this->hasBeenSent() && !$this->tryDbConnection()) {
+ return;
+ }
+
+ if (!$this->migrations()->hasSchema()) {
+ $this->addHtmlHint($this->translate(
+ 'No database schema has been created yet'
+ ), array('name' => 'HINT_schema'));
+
+ $this->addResourceDisplayGroup();
+ $this->setSubmitLabel($this->createDbLabel);
+ return;
+ }
+
+ if ($this->migrations()->hasPendingMigrations()) {
+ $this->addHtmlHint($this->translate(
+ 'There are pending database migrations'
+ ), array('name' => 'HINT_schema'));
+
+ $this->addResourceDisplayGroup();
+ $this->setSubmitLabel($this->migrateDbLabel);
+ return;
+ }
+
+ if (! $this->endpoint && $this->getDb()->hasDeploymentEndpoint()) {
+ $hint = Html::sprintf(
+ $this->translate('Your database looks good, you are ready to %s'),
+ Link::create(
+ $this->translate('start working with the Icinga Director'),
+ 'director',
+ null,
+ ['data-base-target' => '_main']
+ )
+ );
+
+ $this->addHtmlHint($hint, ['name' => 'HINT_ready']);
+ $this->getDisplayGroup('config')->addElements(
+ array($this->getElement('HINT_ready'))
+ );
+
+ return;
+ }
+
+ $this->addResourceDisplayGroup();
+
+ if ($this->getDb()->hasDeploymentEndpoint()) {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Your configuration looks good. Still, you might want to re-run'
+ . ' this kickstart wizard to (re-)import modified or new manually'
+ . ' defined Command definitions or to get fresh new ITL commands'
+ . ' after an Icinga 2 Core upgrade.'
+ ),
+ array('name' => 'HINT_kickstart')
+ // http://docs.icinga.com/icinga2/latest/doc/module/icinga2/chapter/
+ // ... object-types#objecttype-apilistener
+ );
+ } else {
+ $this->addHtmlHint(
+ $this->translate(
+ 'Your installation of Icinga Director has not yet been prepared for'
+ . ' deployments. This kickstart wizard will assist you with setting'
+ . ' up the connection to your Icinga 2 server.'
+ ),
+ array('name' => 'HINT_kickstart')
+ // http://docs.icinga.com/icinga2/latest/doc/module/icinga2/chapter/
+ // ... object-types#objecttype-apilistener
+ );
+ }
+
+ $this->addElement('text', 'endpoint', array(
+ 'label' => $this->translate('Endpoint Name'),
+ 'description' => $this->translate(
+ 'This is the name of the Endpoint object (and certificate name) you'
+ . ' created for your ApiListener object. In case you are unsure what'
+ . ' this means please make sure to read the documentation first'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('text', 'host', array(
+ 'label' => $this->translate('Icinga Host'),
+ 'description' => $this->translate(
+ 'IP address / hostname of your Icinga node. Please note that this'
+ . ' information will only be used for the very first connection to'
+ . ' your Icinga instance. The Director then relies on a correctly'
+ . ' configured Endpoint object. Correctly configures means that either'
+ . ' it\'s name is resolvable or that it\'s host property contains'
+ . ' either an IP address or a resolvable host name. Your Director must'
+ . ' be able to reach this endpoint'
+ ),
+ 'required' => false,
+ ));
+
+ $this->addElement('text', 'port', array(
+ 'label' => $this->translate('Port'),
+ 'value' => '5665',
+ 'description' => $this->translate(
+ 'The port you are going to use. The default port 5665 will be used'
+ . ' if none is set'
+ ),
+ 'required' => false,
+ ));
+
+ $this->addElement('text', 'username', array(
+ 'label' => $this->translate('API user'),
+ 'description' => $this->translate(
+ 'Your Icinga 2 API username'
+ ),
+ 'required' => true,
+ ));
+
+ $this->addElement('password', 'password', array(
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate(
+ 'The corresponding password'
+ ),
+ 'required' => true,
+ ));
+
+ if ($ep = $this->endpoint) {
+ $user = $ep->getApiUser();
+ $this->setDefaults(array(
+ 'endpoint' => $ep->get('object_name'),
+ 'host' => $ep->get('host'),
+ 'port' => $ep->get('port'),
+ 'username' => $user->get('object_name'),
+ 'password' => $user->get('password'),
+ ));
+
+ if (! empty($user->password)) {
+ $this->getElement('password')->setAttrib(
+ 'placeholder',
+ '(use stored password)'
+ )->setRequired(false);
+ }
+ }
+
+ $this->addKickstartDisplayGroup();
+ $this->setSubmitLabel($this->translate('Run import'));
+ }
+
+ /**
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ protected function onSetup()
+ {
+ if ($this->hasBeenSubmitted()) {
+ // Do not hinder the form from being stored
+ return;
+ }
+
+ $this->tryDbConnection();
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function addResourceConfigElements()
+ {
+ $config = $this->config();
+ $resources = $this->enumResources();
+
+ if (!$this->getResourceName()) {
+ $this->addHtmlHint($this->translate(
+ 'No database resource has been configured yet. Please choose a'
+ . ' resource to complete your config'
+ ), array('name' => 'HINT_no_resource'));
+ }
+
+ $this->addElement('select', 'resource', array(
+ 'required' => true,
+ 'label' => $this->translate('DB Resource'),
+ 'multiOptions' => $this->optionalEnum($resources),
+ 'class' => 'autosubmit',
+ 'value' => $config->get('db', 'resource')
+ ));
+
+ if (empty($resources)) {
+ $this->getElement('resource')->addError(
+ $this->translate('This has to be a MySQL or PostgreSQL database')
+ );
+
+ $this->addHtmlHint(Html::sprintf(
+ $this->translate('Please click %s to create new DB resources'),
+ Link::create(
+ $this->translate('here'),
+ 'config/resource',
+ null,
+ ['data-base-target' => '_main']
+ )
+ ));
+ }
+
+ $this->setSubmitLabel($this->storeConfigLabel);
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function addResourceDisplayGroup()
+ {
+ if ($this->dbResourceName !== null) {
+ return;
+ }
+
+ $elements = array(
+ 'HINT_no_resource',
+ 'resource',
+ 'HINT_ready',
+ 'HINT_schema',
+ 'HINT_db_perms',
+ 'HINT_config_store'
+ );
+
+ $this->addDisplayGroup($elements, 'config', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => 40,
+ 'legend' => $this->translate('Database backend')
+ ));
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ protected function addKickstartDisplayGroup()
+ {
+ $elements = array(
+ 'HINT_kickstart', 'endpoint', 'host', 'port', 'username', 'password'
+ );
+
+ $this->addDisplayGroup($elements, 'wizard', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ ),
+ 'order' => 60,
+ 'legend' => $this->translate('Kickstart Wizard')
+ ));
+ }
+
+ /**
+ * @return bool
+ * @throws \Zend_Form_Exception
+ */
+ protected function storeResourceConfig()
+ {
+ $config = $this->config();
+ $value = $this->getValue('resource');
+
+ $config->setSection('db', array('resource' => $value));
+
+ try {
+ $config->saveIni();
+ $this->setSuccessMessage($this->translate('Configuration has been stored'));
+
+ return true;
+ } catch (Exception $e) {
+ $this->getElement('resource')->addError(
+ sprintf(
+ $this->translate(
+ 'Unable to store the configuration to "%s". Please check'
+ . ' file permissions or manually store the content shown below'
+ ),
+ $config->getConfigFile()
+ )
+ );
+ $this->addHtmlHint(
+ Html::tag('pre', null, (string) $config),
+ array('name' => 'HINT_config_store')
+ );
+
+ $this->getDisplayGroup('config')->addElements(
+ array($this->getElement('HINT_config_store'))
+ );
+ $this->removeElement('HINT_ready');
+
+ return false;
+ }
+ }
+
+ public function setEndpoint(IcingaEndpoint $endpoint)
+ {
+ $this->endpoint = $endpoint;
+ return $this;
+ }
+
+ /**
+ * @throws \Icinga\Exception\ProgrammingError
+ * @throws \Zend_Form_Exception
+ */
+ public function onSuccess()
+ {
+ if ($this->getSubmitLabel() === $this->storeConfigLabel) {
+ if ($this->storeResourceConfig()) {
+ parent::onSuccess();
+ } else {
+ return;
+ }
+ }
+
+ if ($this->getSubmitLabel() === $this->createDbLabel
+ || $this->getSubmitLabel() === $this->migrateDbLabel) {
+ $this->migrations()->applyPendingMigrations();
+ parent::onSuccess();
+ }
+
+ $values = $this->getValues();
+ if ($this->endpoint && empty($values['password'])) {
+ $values['password'] = $this->endpoint->getApiUser()->password;
+ }
+
+ $kickstart = new KickstartHelper($this->getDb());
+ unset($values['resource']);
+ $kickstart->setConfig($values)->run();
+
+ parent::onSuccess();
+ }
+
+ public function setDbResourceName($name)
+ {
+ $this->dbResourceName = $name;
+
+ return $this;
+ }
+
+ protected function getResourceName()
+ {
+ if ($this->dbResourceName !== null) {
+ return $this->dbResourceName;
+ }
+
+ if ($this->hasBeenSent()) {
+ $resource = $this->getSentValue('resource');
+ $resources = $this->enumResources();
+ if (in_array($resource, $resources)) {
+ return $resource;
+ } else {
+ return null;
+ }
+ } else {
+ return $this->config()->get('db', 'resource');
+ }
+ }
+
+ public function getDb()
+ {
+ return Db::fromResourceName($this->getResourceName());
+ }
+
+ protected function getResource()
+ {
+ return ResourceFactory::create($this->getResourceName());
+ }
+
+ /**
+ * @return Migrations
+ */
+ protected function migrations()
+ {
+ return new Migrations($this->getDb());
+ }
+
+ public function setModuleConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ protected function config()
+ {
+ if ($this->config === null) {
+ $this->config = Config::module('director');
+ }
+
+ return $this->config;
+ }
+
+ protected function enumResources()
+ {
+ $resources = array();
+ $allowed = array('mysql', 'pgsql');
+
+ foreach (ResourceFactory::getResourceConfigs() as $name => $resource) {
+ if ($resource->get('type') === 'db' && in_array($resource->get('db'), $allowed)) {
+ $resources[$name] = $name;
+ }
+ }
+
+ return $resources;
+ }
+
+ protected function tryDbConnection()
+ {
+ if ($resourceName = $this->getResourceName()) {
+ $resourceConfig = ResourceFactory::getResourceConfig($resourceName);
+ if (!isset($resourceConfig->charset)
+ || !in_array($resourceConfig->charset, array('utf8', 'utf8mb4', 'UTF8', 'UTF-8'))
+ ) {
+ if ($resource = $this->getElement('resource')) {
+ $resource->addError('Please change the encoding for the director database to utf8');
+ } else {
+ $this->addError('Please change the encoding for the director database to utf8');
+ }
+ }
+
+ $resource = $this->getResource();
+ $db = $resource->getDbAdapter();
+
+ try {
+ $db->fetchOne('SELECT 1');
+ return true;
+ } catch (Exception $e) {
+ $this->getElement('resource')
+ ->addError('Could not connect to database: ' . $e->getMessage());
+
+ $hint = $this->translate(
+ 'Please make sure that your database exists and your user has'
+ . ' been granted enough permissions'
+ );
+
+ $this->addHtmlHint($hint, array('name' => 'HINT_db_perms'));
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/application/forms/RemoveLinkForm.php b/application/forms/RemoveLinkForm.php
new file mode 100644
index 0000000..6f0c7cc
--- /dev/null
+++ b/application/forms/RemoveLinkForm.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\IcingaWeb2\Icon;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class RemoveLinkForm extends DirectorForm
+{
+ private $label;
+
+ private $title;
+
+ private $onSuccessAction;
+
+ public function __construct($label, $title, $action, $params = [])
+ {
+ // Required to detect the right instance
+ $this->formName = 'RemoveSet' . sha1(json_encode($params));
+ parent::__construct([
+ 'style' => 'float: right',
+ 'data-base-target' => '_self'
+ ]);
+ $this->label = $label;
+ $this->title = $title;
+ foreach ($params as $name => $value) {
+ $this->addHidden($name, $value);
+ }
+ $this->setAction($action);
+ }
+
+ public function runOnSuccess($action)
+ {
+ $this->onSuccessAction = $action;
+
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->addHtml(Icon::create('cancel'));
+ $this->addSubmitButton($this->label, [
+ 'class' => 'link-button',
+ 'title' => $this->title,
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ if ($this->onSuccessAction !== null) {
+ $func = $this->onSuccessAction;
+ $func();
+ $this->redirectOnSuccess(
+ $this->translate('Service Set has been removed')
+ );
+ }
+ }
+}
diff --git a/application/forms/RestoreBasketForm.php b/application/forms/RestoreBasketForm.php
new file mode 100644
index 0000000..90d5b38
--- /dev/null
+++ b/application/forms/RestoreBasketForm.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
+use Icinga\Module\Director\Web\Controller\Extension\DirectorDb;
+use Icinga\Module\Director\Web\Form\QuickForm;
+
+class RestoreBasketForm extends QuickForm
+{
+ use DirectorDb;
+
+ /** @var BasketSnapshot */
+ private $snapshot;
+
+ public function setSnapshot(BasketSnapshot $snapshot)
+ {
+ $this->snapshot = $snapshot;
+
+ return $this;
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ * @return Auth
+ */
+ protected function Auth()
+ {
+ return Auth::getInstance();
+ }
+
+ /**
+ * @return Config
+ */
+ protected function Config()
+ {
+ // @codingStandardsIgnoreEnd
+ return Config::module('director');
+ }
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $allowedDbs = $this->listAllowedDbResourceNames();
+ $this->addElement('select', 'target_db', [
+ 'label' => $this->translate('Target DB'),
+ 'description' => $this->translate('Restore to this target Director DB'),
+ 'multiOptions' => $allowedDbs,
+ 'value' => $this->getRequest()->getParam('target_db', $this->getFirstDbResourceName()),
+ 'class' => 'autosubmit',
+ ]);
+
+ $this->setSubmitLabel($this->translate('Restore'));
+ }
+
+ public function getDb()
+ {
+ return Db::fromResourceName($this->getValue('target_db'));
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function onSuccess()
+ {
+ $this->snapshot->restoreTo($this->getDb());
+ $this->setSuccessUrl($this->getSuccessUrl()->with('target_db', $this->getValue('target_db')));
+ $this->setSuccessMessage(sprintf('Restored to %s', $this->getValue('target_db')));
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/RestoreObjectForm.php b/application/forms/RestoreObjectForm.php
new file mode 100644
index 0000000..e665d65
--- /dev/null
+++ b/application/forms/RestoreObjectForm.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class RestoreObjectForm extends DirectorForm
+{
+ /** @var IcingaObject */
+ protected $object;
+
+ public function setup()
+ {
+ $this->addSubmitButton($this->translate('Restore former object'));
+ }
+
+ public function onSuccess()
+ {
+ $object = $this->object;
+ $name = $object->getObjectName();
+ $db = $this->db;
+
+ $keyParams = $object->getKeyParams();
+
+ if ($object->supportsApplyRules() && $object->get('object_type') === 'apply') {
+ // TODO: not all apply should be considered unique by name + object_type
+ $query = $db->getDbAdapter()
+ ->select()
+ ->from($object->getTableName())
+ ->where('object_type = ?', 'apply')
+ ->where('object_name = ?', $name);
+
+ $rules = $object::loadAll($db, $query);
+
+ if (empty($rules)) {
+ $existing = null;
+ } elseif (count($rules) === 1) {
+ $existing = current($rules);
+ } else {
+ // TODO: offer drop down?
+ throw new NotImplementedError(
+ "Found multiple apply rule matching name '%s', can not restore!",
+ $name
+ );
+ }
+ } else {
+ try {
+ $existing = $object::load($keyParams, $db);
+ } catch (NotFoundError $e) {
+ $existing = null;
+ }
+ }
+
+ if ($existing !== null) {
+ $typeExisting = $existing->get('object_type');
+ $typeObject = $object->get('object_type');
+ if ($typeExisting !== $typeObject) {
+ // Not sure when that may occur
+ throw new NotImplementedError(
+ 'Found existing object has a mismatching object_type: %s != %s',
+ $typeExisting,
+ $typeObject
+ );
+ }
+
+ $existing->replaceWith($object);
+
+ if ($existing->hasBeenModified()) {
+ $msg = $this->translate('Object has been restored');
+ $existing->store();
+ } else {
+ $msg = $this->translate(
+ 'Nothing to do, restore would not modify the current object'
+ );
+ }
+ } else {
+ $msg = $this->translate('Object has been re-created');
+ $object->store($db);
+ }
+
+ $this->redirectOnSuccess($msg);
+ }
+
+ public function setObject(IcingaObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+}
diff --git a/application/forms/SelfServiceSettingsForm.php b/application/forms/SelfServiceSettingsForm.php
new file mode 100644
index 0000000..4471bcf
--- /dev/null
+++ b/application/forms/SelfServiceSettingsForm.php
@@ -0,0 +1,306 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class SelfServiceSettingsForm extends DirectorForm
+{
+ /** @var Settings */
+ protected $settings;
+
+ public function setup()
+ {
+ $settings = $this->settings;
+ $this->addElement('select', 'agent_name', [
+ 'label' => $this->translate('Host Name'),
+ 'description' => $this->translate(
+ 'What to use as your Icinga 2 Agent\'s Host Name'
+ ),
+ 'multiOptions' => [
+ 'fqdn' => $this->translate('Fully qualified domain name (FQDN)'),
+ 'hostname' => $this->translate('Host name (local part, without domain)'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/agent_name')
+ ]);
+
+ $this->addElement('select', 'transform_hostname', [
+ 'label' => $this->translate('Transform Host Name'),
+ 'description' => $this->translate(
+ 'Whether to adjust your host name'
+ ),
+ 'multiOptions' => [
+ '0' => $this->translate('Do not transform at all'),
+ '1' => $this->translate('Transform to lowercase'),
+ '2' => $this->translate('Transform to uppercase'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/transform_hostname')
+ ]);
+ $this->addElement('select', 'resolve_parent_host', [
+ 'label' => $this->translate('Transform Parent Host to IP'),
+ 'description' => $this->translate(
+ 'This is only important in case your master/satellite nodes do not'
+ . ' have IP addresses as their "host" property. The Agent can be'
+ . ' told to issue related DNS lookups on it\' own'
+ ),
+ 'multiOptions' => [
+ '0' => $this->translate("Don't care, my host settings are fine"),
+ '1' => $this->translate('My Agents should use DNS to look up Endpoint names'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/resolve_parent_host')
+ ]);
+
+ $this->addElement('extensibleSet', 'global_zones', [
+ 'label' => $this->translate('Global Zones'),
+ 'description' => $this->translate(
+ 'To ensure downloaded packages are build by the Icinga Team'
+ . ' and not compromised by third parties, you will be able'
+ . ' to provide an array of SHA1 hashes here. In case you have'
+ . ' defined any hashses, the module will not continue with'
+ . ' updating / installing the Agent in case the SHA1 hash of'
+ . ' the downloaded MSI package is not matching one of the'
+ . ' provided hashes of this setting'
+ ),
+ 'multiOptions' => $this->enumGlobalZones(),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/global_zones'),
+ ]);
+
+ $this->addElement('select', 'download_type', [
+ 'label' => $this->translate('Installation Source'),
+ 'description' => $this->translate(
+ 'You might want to let the generated Powershell script install'
+ . ' the Icinga 2 Agent in an automated way. If so, please choose'
+ . ' where your Windows nodes should fetch the Agent installer'
+ ),
+ 'multiOptions' => [
+ null => $this->translate('- no automatic installation -'),
+ // TODO: not yet
+ // 'director' => $this->translate('Download via the Icinga Director'),
+ 'icinga' => $this->translate('Download from packages.icinga.com'),
+ 'url' => $this->translate('Download from a custom url'),
+ 'file' => $this->translate('Use a local file or network share'),
+ ],
+ 'value' => $settings->getStoredOrDefaultValue('self-service/download_type'),
+ 'class' => 'autosubmit'
+ ]);
+
+ $downloadType = $this->getSentValue(
+ 'download_type',
+ $settings->getStoredOrDefaultValue('self-service/download_type')
+ );
+
+ if ($downloadType) {
+ $this->addInstallSettings($downloadType, $settings);
+ }
+
+ $this->addEventuallyConfiguredBoolean('flush_api_dir', [
+ 'label' => $this->translate('Flush API directory'),
+ 'description' => $this->translate(
+ 'In case the Icinga Agent will accept configuration from the parent'
+ . ' Icinga 2 system, it will possibly write data to /var/lib/icinga2/api/*.'
+ . ' By setting this parameter to true, all content inside the api directory'
+ . ' will be flushed before an eventual restart of the Icinga 2 Agent'
+ ),
+ 'required' => true,
+ ]);
+ }
+
+ protected function addInstallSettings($downloadType, Settings $settings)
+ {
+ $this->addElement('text', 'download_url', [
+ 'label' => $this->translate('Source Path'),
+ 'description' => $this->translate(
+ 'Define a download Url or local directory from which the a specific'
+ . ' Icinga 2 Agent MSI Installer package should be fetched. Please'
+ . ' ensure to only define the base download Url or Directory. The'
+ . ' Module will generate the MSI file name based on your operating'
+ . ' system architecture and the version to install. The Icinga 2 MSI'
+ . ' Installer name is internally build as follows:'
+ . ' Icinga2-v[InstallAgentVersion]-[OSArchitecture].msi (full example:'
+ . ' Icinga2-v2.6.3-x86_64.msi)'
+ ),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/download_url'),
+ ]);
+
+ // TODO: offer to check for available versions
+ if ($downloadType === 'icinga') {
+ $el = $this->getElement('download_url');
+ $el->setAttrib('disabled', 'disabled');
+ $value = 'https://packages.icinga.com/windows/';
+ $el->setValue($value);
+ $this->setSentValue('download_url', $value);
+ }
+ if ($downloadType === 'director') {
+ $el = $this->getElement('download_url');
+ $el->setAttrib('disabled', 'disabled');
+
+ $r = $this->getRequest();
+ $scheme = $r->getServer('HTTP_X_FORWARDED_PROTO', $r->getScheme());
+
+ $value = sprintf(
+ '%s://%s%s/director/download/windows/',
+ $scheme,
+ $r->getHttpHost(),
+ $this->getRequest()->getBaseUrl()
+ );
+ $el->setValue($value);
+ $this->setSentValue('download_url', $value);
+ }
+
+ $this->addElement('text', 'agent_version', [
+ 'label' => $this->translate('Agent Version'),
+ 'description' => $this->translate(
+ 'In case the Icinga 2 Agent should be automatically installed,'
+ . ' this has to be a string value like: 2.6.3'
+ ),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/agent_version'),
+ 'required' => true,
+ ]);
+
+ $hashes = $settings->getStoredOrDefaultValue('self-service/installer_hashes');
+ $this->addElement('extensibleSet', 'installer_hashes', [
+ 'label' => $this->translate('Installer Hashes'),
+ 'description' => $this->translate(
+ 'To ensure downloaded packages are build by the Icinga Team'
+ . ' and not compromised by third parties, you will be able'
+ . ' to provide an array of SHA1 hashes here. In case you have'
+ . ' defined any hashses, the module will not continue with'
+ . ' updating / installing the Agent in case the SHA1 hash of'
+ . ' the downloaded MSI package is not matching one of the'
+ . ' provided hashes of this setting'
+ ),
+ 'value' => $hashes,
+ ]);
+
+ $this->addElement('text', 'icinga_service_user', [
+ 'label' => $this->translate('Service User'),
+ 'description' => $this->translate(
+ 'The user that should run the Icinga 2 service on Windows.'
+ ),
+ 'value' => $settings->getStoredOrDefaultValue('self-service/icinga_service_user'),
+ ]);
+
+ $this->addEventuallyConfiguredBoolean('allow_updates', [
+ 'label' => $this->translate('Allow Updates'),
+ 'description' => $this->translate(
+ 'In case the Icinga 2 Agent is already installed on the system,'
+ . ' this parameter will allow you to configure if you wish to'
+ . ' upgrade / downgrade to a specified version with the as well.'
+ ),
+ 'required' => true,
+ ]);
+
+ $this->addNscpSettings();
+ }
+
+ protected function addNscpSettings()
+ {
+ $this->addEventuallyConfiguredBoolean('install_nsclient', [
+ 'label' => $this->translate('Install NSClient++'),
+ 'description' => $this->translate(
+ 'Also install NSClient++. It can be used through the Icinga Agent'
+ . ' and comes with a bunch of additional Check Plugins'
+ ),
+ 'required' => true,
+ ]);
+ /*
+ * TODO: eventually add those:
+ if ($settings->get('self-service/install_nsclient') === 'y') {
+ $params['install_nsclient'] = true;
+ $this->addBooleanSettingsToParams($settings, [
+ 'nsclient_add_defaults',
+ 'nsclient_firewall',
+ 'nsclient_service',
+ ], $params);
+
+
+ $this->addStringSettingsToParams($settings, [
+ 'nsclient_directory',
+ 'nsclient_installer_path'
+ ], $params);
+ }
+ */
+ }
+
+ public static function create(Db $db, Settings $settings)
+ {
+ return static::load()->setDb($db)->setSettings($settings);
+ }
+
+ protected function addEventuallyConfiguredBoolean($name, $params)
+ {
+ $key = "self-service/$name";
+ $value = $this->settings->getStoredValue($key);
+ $params['value'] = $value;
+ $params['multiOptions'] = $this->eventuallyConfiguredEnum($name, [
+ 'y' => $this->translate('Yes'),
+ 'n' => $this->translate('No'),
+ ]);
+
+ return $this->addElement('select', $name, $params);
+ }
+
+ protected function eventuallyConfiguredEnum($name, $enum)
+ {
+ $key = "self-service/$name";
+ $default = $this->settings->getDefaultValue($key);
+ if ($default === null) {
+ return [
+ null => $this->translate('- please choose -')
+ ] + $enum;
+ } else {
+ return [
+ null => sprintf($this->translate('%s (default)'), $enum[$default])
+ ] + $enum;
+ }
+ }
+
+ protected function setSentValue($key, $value)
+ {
+ $this->getRequest()->setPost($key, $value);
+ return $this;
+ }
+
+ protected function enumGlobalZones()
+ {
+ $db = $this->getDb()->getDbAdapter();
+ $zones = $db->fetchCol(
+ $db->select()->from('icinga_zone', 'object_name')
+ ->where('disabled = ?', 'n')
+ ->where('is_global = ?', 'y')
+ ->order('object_name')
+ );
+
+ return array_combine($zones, $zones);
+ }
+
+ public function setSettings(Settings $settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ try {
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === '') {
+ $value = null;
+ }
+
+ $this->settings->set("self-service/$key", $value);
+ }
+
+ $this->setSuccessMessage($this->translate(
+ 'Self Service Settings have been stored'
+ ));
+
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addException($e);
+ }
+ }
+}
diff --git a/application/forms/SettingsForm.php b/application/forms/SettingsForm.php
new file mode 100644
index 0000000..f6ba654
--- /dev/null
+++ b/application/forms/SettingsForm.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Settings;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class SettingsForm extends DirectorForm
+{
+ /** @var Settings */
+ protected $settings;
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $settings = $this->settings;
+
+ $this->addHtmlHint(
+ $this->translate(
+ 'Please only change those settings in case you are really sure'
+ . ' that you are required to do so. Usually the defaults chosen'
+ . ' by the Icinga Director should make a good fit for your'
+ . ' environment.'
+ )
+ );
+ $globalZones = $this->eventuallyConfiguredEnum('default_global_zone', $this->enumGlobalZones());
+
+ $this->addElement('select', 'default_global_zone', array(
+ 'label' => $this->translate('Default global zone'),
+ 'multiOptions' => $globalZones,
+ 'description' => $this->translate(
+ 'Icinga Director decides to deploy objects like CheckCommands'
+ . ' to a global zone. This defaults to "director-global" but'
+ . ' might be adjusted to a custom Zone name'
+ ),
+ 'value' => $settings->getStoredValue('default_global_zone')
+ ));
+
+ $this->addElement('text', 'icinga_package_name', array(
+ 'label' => $this->translate('Icinga Package Name'),
+ 'description' => $this->translate(
+ 'The Icinga Package name Director uses to deploy it\'s configuration.'
+ . ' This defaults to "director" and should not be changed unless'
+ . ' you really know what you\'re doing'
+ ),
+ 'placeholder' => $settings->get('icinga_package_name'),
+ 'value' => $settings->getStoredValue('icinga_package_name')
+ ));
+
+ $this->addElement('select', 'disable_all_jobs', array(
+ 'label' => $this->translate('Disable all Jobs'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'disable_all_jobs',
+ array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Whether all configured Jobs should be disabled'
+ ),
+ 'value' => $settings->getStoredValue('disable_all_jobs')
+ ));
+
+ $this->addElement('select', 'enable_audit_log', array(
+ 'label' => $this->translate('Enable audit log'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'enable_audit_log',
+ array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'All changes are tracked in the Director database. In addition'
+ . ' you might also want to send an audit log through the Icinga'
+ . " Web 2 logging mechanism. That way all changes would be"
+ . ' written to either Syslog or the configured log file. When'
+ . ' enabling this please make sure that you configured Icinga'
+ . ' Web 2 to log at least at "informational" level.'
+ ),
+ 'value' => $settings->getStoredValue('enable_audit_log')
+ ));
+
+ if ($settings->getStoredValue('ignore_bug7530')) {
+ // Show this only for those who touched this setting
+ $this->addElement('select', 'ignore_bug7530', array(
+ 'label' => $this->translate('Ignore Bug #7530'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'ignore_bug7530',
+ array(
+ 'n' => $this->translate('No'),
+ 'y' => $this->translate('Yes'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Icinga v2.11.0 breaks some configurations, the Director will'
+ . ' warn you before every deployment in case your config is'
+ . ' affected. This setting allows to hide this warning.'
+ ),
+ 'value' => $settings->getStoredValue('ignore_bug7530')
+ ));
+ }
+
+ $this->addBoolean('feature_custom_endpoint', [
+ 'label' => $this->translate('Feature: Custom Endpoint Name'),
+ 'description' => $this->translate(
+ 'Enabled the feature for custom endpoint names,'
+ . ' where you can choose a different name for the generated endpoint object.'
+ . ' This uses some Icinga config snippets and a special custom variable.'
+ . ' Please do NOT enable this, unless you really need divergent endpoint names!'
+ ),
+ 'value' => $settings->getStoredValue('feature_custom_endpoint')
+ ]);
+
+
+ $this->addElement('select', 'config_format', array(
+ 'label' => $this->translate('Configuration format'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'config_format',
+ array(
+ 'v2' => $this->translate('Icinga v2.x'),
+ 'v1' => $this->translate('Icinga v1.x'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Default configuration format. Please note that v1.x is for'
+ . ' special transitional projects only and completely'
+ . ' unsupported. There are no plans to make Director a first-'
+ . 'class configuration backends for Icinga 1.x'
+ ),
+ 'class' => 'autosubmit',
+ 'value' => $settings->getStoredValue('config_format')
+ ));
+
+ $this->setSubmitLabel($this->translate('Store'));
+
+ if ($this->hasBeenSent()) {
+ if ($this->getSentValue('config_format') !== 'v1') {
+ return;
+ }
+ } elseif ($settings->getStoredValue('config_format') !== 'v1') {
+ return;
+ }
+
+ $this->addElement('select', 'deployment_mode_v1', array(
+ 'label' => $this->translate('Deployment mode'),
+ 'multiOptions' => $this->eventuallyConfiguredEnum(
+ 'deployment_mode_v1',
+ array(
+ 'active-passive' => $this->translate('Active-Passive'),
+ 'masterless' => $this->translate('Master-less'),
+ )
+ ),
+ 'description' => $this->translate(
+ 'Deployment mode for Icinga 1 configuration'
+ ),
+ 'value' => $settings->getStoredValue('deployment_mode_v1')
+ ));
+
+ $this->addElement('text', 'deployment_path_v1', array(
+ 'label' => $this->translate('Deployment Path'),
+ 'description' => $this->translate(
+ 'Local directory to deploy Icinga 1.x configuration.'
+ . ' Must be writable by icingaweb2.'
+ . ' (e.g. /etc/icinga/director)'
+ ),
+ 'value' => $settings->getStoredValue('deployment_path_v1')
+ ));
+
+ $this->addElement('text', 'activation_script_v1', array(
+ 'label' => $this->translate('Activation Tool'),
+ 'description' => $this->translate(
+ 'Script or tool to call when activating a new configuration stage.'
+ . ' (e.g. sudo /usr/local/bin/icinga-director-activate)'
+ . ' (name of the stage will be the argument for the script)'
+ ),
+ 'value' => $settings->getStoredValue('activation_script_v1')
+ ));
+ }
+
+ protected function eventuallyConfiguredEnum($name, $enum)
+ {
+ if (array_key_exists($name, $enum)) {
+ $default = sprintf(
+ $this->translate('%s (default)'),
+ $enum[$this->settings->getDefaultValue($name)]
+ );
+ } else {
+ $default = $this->translate('- please choose -');
+ }
+
+ return [null => $default] + $enum;
+ }
+
+ public function setSettings(Settings $settings)
+ {
+ $this->settings = $settings;
+ return $this;
+ }
+
+ protected function enumGlobalZones()
+ {
+ $db = $this->settings->getDb();
+ $zones = $db->fetchCol(
+ $db->select()->from('icinga_zone', 'object_name')
+ ->where('disabled = ?', 'n')
+ ->where('is_global = ?', 'y')
+ ->order('object_name')
+ );
+
+ return array_combine($zones, $zones);
+ }
+
+ public function onSuccess()
+ {
+ try {
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === '') {
+ $value = null;
+ }
+
+ $this->settings->set($key, $value);
+ }
+
+ $this->setSuccessMessage($this->translate(
+ 'Settings have been stored'
+ ));
+
+ parent::onSuccess();
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ }
+ }
+}
diff --git a/application/forms/SyncCheckForm.php b/application/forms/SyncCheckForm.php
new file mode 100644
index 0000000..8fb3bd0
--- /dev/null
+++ b/application/forms/SyncCheckForm.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Objects\DirectorActivityLog;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Web\Form\DirectorForm;
+
+class SyncCheckForm extends DirectorForm
+{
+ /** @var SyncRule */
+ protected $rule;
+
+ public function setSyncRule(SyncRule $rule)
+ {
+ $this->rule = $rule;
+ return $this;
+ }
+
+ public function setup()
+ {
+ $this->submitLabel = false;
+ $this->addElement('submit', 'submit', array(
+ 'label' => $this->translate('Check for changes'),
+ 'decorators' => array('ViewHelper')
+ ));
+ }
+
+ public function onSuccess()
+ {
+ if ($this->rule->checkForChanges()) {
+ $this->notifySuccess(
+ $this->translate(('This Sync Rule would apply new changes'))
+ );
+ $sum = [
+ DirectorActivityLog::ACTION_CREATE => 0,
+ DirectorActivityLog::ACTION_MODIFY => 0,
+ DirectorActivityLog::ACTION_DELETE => 0
+ ];
+
+ // TODO: Preview them? Like "hosta, hostb and 4 more would be...
+ foreach ($this->rule->getExpectedModifications() as $object) {
+ if ($object->shouldBeRemoved()) {
+ $sum[DirectorActivityLog::ACTION_DELETE]++;
+ } elseif (! $object->hasBeenLoadedFromDb()) {
+ $sum[DirectorActivityLog::ACTION_CREATE]++;
+ } elseif ($object->hasBeenModified()) {
+ $sum[DirectorActivityLog::ACTION_MODIFY]++;
+ }
+ }
+
+ /**
+ if ($sum['modify'] === 1) {
+ $html .= $this->translate('One object would be modified'
+ } elseif ($sum['modify'] > 1) {
+ }
+ */
+ $html = '<pre>' . print_r($sum, 1) . '</pre>';
+
+ $this->addHtml($html);
+ } elseif ($this->rule->get('sync_state') === 'in-sync') {
+ $this->notifySuccess(
+ $this->translate('Nothing would change, this rule is still in sync')
+ );
+ } else {
+ $this->addError($this->translate('Checking this sync rule failed'));
+ }
+ }
+}
diff --git a/application/forms/SyncPropertyForm.php b/application/forms/SyncPropertyForm.php
new file mode 100644
index 0000000..720237e
--- /dev/null
+++ b/application/forms/SyncPropertyForm.php
@@ -0,0 +1,444 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Exception;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\SyncProperty;
+use Icinga\Module\Director\Objects\SyncRule;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class SyncPropertyForm extends DirectorObjectForm
+{
+ /**
+ * @var SyncRule
+ */
+ private $rule;
+
+ /** @var ImportSource */
+ private $importSource;
+
+ /** @var ImportSourceHook */
+ private $importSourceHook;
+
+ private $dummyObject;
+
+ const EXPRESSION = '__EXPRESSION__';
+
+ /**
+ * @throws \Zend_Form_Exception
+ */
+ public function setup()
+ {
+ $this->addHidden('rule_id', $this->rule->get('id'));
+
+ $this->addElement('select', 'source_id', array(
+ 'label' => $this->translate('Source Name'),
+ 'multiOptions' => $this->enumImportSource(),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ));
+ if (! $this->hasObject() && ! $this->getSentValue('source_id')) {
+ return;
+ }
+
+ $this->addElement('select', 'destination_field', array(
+ 'label' => $this->translate('Destination Field'),
+ 'multiOptions' => $this->optionalEnum($this->listDestinationFields()),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ));
+
+ if ($this->getSentValue('destination_field')) {
+ $destination = $this->getSentValue('destination_field');
+ } elseif ($this->hasObject()) {
+ $destination = $this->getObject()->destination_field;
+ } else {
+ return;
+ }
+
+ $isCustomvar = substr($destination, 0, 5) === 'vars.';
+
+ if ($isCustomvar) {
+ $varname = substr($destination, 5);
+ $this->addElement('text', 'customvar', array(
+ 'label' => $this->translate('Custom variable'),
+ 'required' => true,
+ 'ignore' => true,
+ ));
+
+ if ($varname !== '*') {
+ $this->setElementValue('destination_field', 'vars.*');
+ $this->setElementValue('customvar', $varname);
+ if ($this->hasObject()) {
+ $this->getObject()->destination_field = 'vars.*';
+ }
+ }
+ }
+
+ $this->addSourceColumnElement($destination);
+
+ $this->addElement('YesNo', 'use_filter', array(
+ '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');
+ }
+ } else {
+ $expression = $this->getObject()->filter_expression;
+ $useFilter = ($expression === null || strlen($expression) === 0) ? 'n' : 'y';
+ $this->setElementValue('use_filter', $useFilter);
+ }
+
+ if ($useFilter === 'y') {
+ $this->addElement('text', 'filter_expression', array(
+ '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)'
+ ),
+ 'required' => true,
+ // TODO: validate filter
+ ));
+ }
+
+ if ($isCustomvar || $destination === 'vars') {
+ $this->addElement('select', 'merge_policy', array(
+ 'label' => $this->translate('Merge Policy'),
+ 'description' => $this->translate(
+ 'Whether you want to merge or replace the destination field.'
+ . ' Makes no difference for strings'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum(array(
+ 'merge' => 'merge',
+ 'override' => 'replace'
+ ))
+ ));
+ } else {
+ $this->addHidden('merge_policy', 'override');
+ }
+
+ $this->setButtons();
+ }
+
+ protected function hasSubOption($options, $key)
+ {
+ foreach ($options as $mainKey => $sub) {
+ if (! is_array($sub)) {
+ // null -> please choose - or similar
+ continue;
+ }
+
+ if (array_key_exists($key, $sub)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $destination
+ * @return $this
+ * @throws \Zend_Form_Exception
+ */
+ protected function addSourceColumnElement($destination)
+ {
+ $error = false;
+
+ $srcTitle = $this->translate('Source columns');
+ try {
+ $columns[$srcTitle] = $this->listSourceColumns();
+ natsort($columns[$srcTitle]);
+ } catch (Exception $e) {
+ $srcTitle .= sprintf(' (%s)', $this->translate('failed to fetch'));
+ $columns[$srcTitle] = array();
+ $error = sprintf(
+ $this->translate('Unable to fetch data: %s'),
+ $e->getMessage()
+ );
+ }
+
+ if ($destination === 'import') {
+ $this->addIcingaTempateColumns($columns);
+ } elseif ($destination === 'list_id') {
+ $this->addDatalistsColumns($columns);
+ }
+
+ $xpTitle = $this->translate('Expert mode');
+ $columns[$xpTitle][self::EXPRESSION] = $this->translate('Custom expression');
+
+ $this->addElement('select', 'source_column', array(
+ 'label' => $this->translate('Source Column'),
+ 'multiOptions' => $this->optionalEnum($columns),
+ 'required' => true,
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ ));
+
+ if ($error) {
+ $this->getElement('source_column')->addError($error);
+ }
+
+ $showExpression = false;
+
+ if ($this->hasBeenSent()) {
+ $sentValue = $this->getSentValue('source_column');
+ if ($sentValue === self::EXPRESSION) {
+ $showExpression = true;
+ }
+ } elseif ($this->hasObject()) {
+ $objectValue = $this->getObject()->source_expression;
+ if ($this->hasSubOption($columns, $objectValue)) {
+ $this->setElementValue('source_column', $objectValue);
+ } else {
+ $this->setElementValue('source_column', self::EXPRESSION);
+ $showExpression = true;
+ }
+ }
+
+ if ($showExpression) {
+ $this->addElement('text', 'source_expression', array(
+ 'label' => $this->translate('Source Expression'),
+ 'description' => $this->translate(
+ 'A custom string. Might contain source columns, please use placeholders'
+ . ' of the form ${columnName} in such case. Structured data sources'
+ . ' can be referenced as ${columnName.sub.key}'
+ ),
+ 'required' => true,
+ ));
+ }
+
+
+ return $this;
+ }
+
+ protected function addIcingaTempateColumns(&$columns)
+ {
+ $funcTemplates = 'enum' . ucfirst($this->rule->get('object_type')) . 'Templates';
+ if (method_exists($this->db, $funcTemplates)) {
+ $templates = $this->db->$funcTemplates();
+ if (! empty($templates)) {
+ $templates = array_combine($templates, $templates);
+ }
+
+ $title = $this->translate('Existing templates');
+ $columns[$title] = $templates;
+ natsort($columns[$title]);
+ }
+ }
+
+ protected function addDatalistsColumns(&$columns)
+ {
+ // Clear other columns, we don't allow them right now
+ $columns = [];
+ $db = $this->db->getDbAdapter();
+ $enum = $db->fetchPairs(
+ $db->select()->from('director_datalist', ['id', 'list_name'])->order('list_name')
+ );
+
+ $columns[$this->translate('Existing Data Lists')] = $enum;
+ }
+
+ protected function enumImportSource()
+ {
+ $sources = $this->db->enumImportSource();
+ $usedIds = $this->rule->listInvolvedSourceIds();
+ if (empty($usedIds)) {
+ return $this->optionalEnum($sources);
+ }
+ $usedSources = array();
+ foreach ($usedIds as $id) {
+ $usedSources[$id] = $sources[$id];
+ unset($sources[$id]);
+ }
+
+ if (empty($sources)) {
+ return $this->optionalEnum($usedSources);
+ }
+
+ return $this->optionalEnum(
+ array(
+ $this->translate('Used sources') => $usedSources,
+ $this->translate('Other sources') => $sources
+ )
+ );
+ }
+
+ /**
+ * @return array
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function listSourceColumns()
+ {
+ $columns = array();
+ $source = $this->getImportSource();
+ $hook = $this->getImportSourceHook();
+ foreach ($hook->listColumns() as $col) {
+ $columns['${' . $col . '}'] = $col;
+ }
+
+ foreach ($source->listModifierTargetProperties() as $property) {
+ $columns['${' . $property . '}'] = $property;
+ }
+
+ return $columns;
+ }
+
+ protected function listDestinationFields()
+ {
+ $props = [];
+ $special = [];
+ $dummy = $this->dummyObject();
+
+ if ($dummy instanceof IcingaObject) {
+ if ($dummy->supportsCustomVars()) {
+ $special['vars.*'] = $this->translate('Custom variable (vars.)');
+ $special['vars'] = $this->translate('All custom variables (vars)');
+ }
+ if ($dummy->supportsImports()) {
+ $special['import'] = $this->translate('Inheritance (import)');
+ }
+ if ($dummy->supportsArguments()) {
+ $special['arguments'] = $this->translate('Arguments');
+ }
+ if ($dummy->supportsGroups()) {
+ $special['groups'] = $this->translate('Group membership');
+ }
+ if ($dummy->supportsRanges()) {
+ $special['ranges'] = $this->translate('Time ranges');
+ }
+ }
+
+ foreach ($dummy->listProperties() as $prop) {
+ if ($dummy instanceof IcingaObject && $prop === 'id') {
+ continue;
+ }
+
+ // TODO: allow those fields, but munge them (store ids)
+ //if (preg_match('~_id$~', $prop)) continue;
+ if (substr($prop, -3) === '_id') {
+ $short = substr($prop, 0, -3);
+ if ($dummy instanceof IcingaObject) {
+ if ($dummy->hasRelation($short)) {
+ $prop = $short;
+ } else {
+ continue;
+ }
+ }
+ }
+
+ $props[$prop] = $prop;
+ }
+
+ if ($dummy instanceof IcingaObject) {
+ foreach ($dummy->listMultiRelations() as $prop) {
+ $props[$prop] = sprintf('%s (%s)', $prop, $this->translate('a list'));
+ }
+ }
+
+ ksort($props);
+
+ $result = [];
+ if (! empty($special)) {
+ $result[$this->translate('Special properties')] = $special;
+ }
+ if (! empty($props)) {
+ $result[$this->translate('Object properties')] = $props;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return ImportSource
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getImportSource()
+ {
+ if ($this->importSource === null) {
+ if ($this->hasObject()) {
+ $id = (int) $this->object->get('source_id');
+ } else {
+ $id = (int) $this->getSentValue('source_id');
+ }
+ $this->importSource = ImportSource::loadWithAutoIncId($id, $this->db);
+ }
+
+ return $this->importSource;
+ }
+
+ /**
+ * @return ImportSourceHook
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getImportSourceHook()
+ {
+ if ($this->importSourceHook === null) {
+ $this->importSourceHook = ImportSourceHook::loadByName(
+ $this->getImportSource()->get('source_name'),
+ $this->db
+ );
+ }
+
+ return $this->importSourceHook;
+ }
+
+ public function onSuccess()
+ {
+ /** @var SyncProperty $object */
+ $object = $this->getObject();
+ $object->set('rule_id', $this->rule->get('id')); // ?!
+
+ if ($this->getValue('use_filter') === 'n') {
+ $object->set('filter_expression', null);
+ }
+
+ $sourceColumn = $this->getValue('source_column');
+ $this->removeElement('source_column');
+
+ if ($sourceColumn !== self::EXPRESSION) {
+ $object->set('source_expression', $sourceColumn);
+ }
+
+ $destination = $this->getValue('destination_field');
+ if ($destination === 'vars.*') {
+ $destination = $this->getValue('customvar');
+ $object->set('destination_field', 'vars.' . $destination);
+ }
+
+ return parent::onSuccess();
+ }
+
+ protected function dummyObject()
+ {
+ if ($this->dummyObject === null) {
+ $this->dummyObject = IcingaObject::createByType(
+ $this->rule->get('object_type'),
+ array(),
+ $this->db
+ );
+ }
+
+ return $this->dummyObject;
+ }
+
+ public function setRule(SyncRule $rule)
+ {
+ $this->rule = $rule;
+ return $this;
+ }
+}
diff --git a/application/forms/SyncRuleForm.php b/application/forms/SyncRuleForm.php
new file mode 100644
index 0000000..d88e493
--- /dev/null
+++ b/application/forms/SyncRuleForm.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use Icinga\Module\Director\Web\Form\DirectorObjectForm;
+
+class SyncRuleForm extends DirectorObjectForm
+{
+ public function setup()
+ {
+ $availableTypes = [
+ 'host' => $this->translate('Host'),
+ 'hostgroup' => $this->translate('Host Group'),
+ 'service' => $this->translate('Service'),
+ 'servicegroup' => $this->translate('Service Group'),
+ 'serviceSet' => $this->translate('Service Set'),
+ 'user' => $this->translate('User'),
+ 'usergroup' => $this->translate('User Group'),
+ 'datalistEntry' => $this->translate('Data List Entry'),
+ 'command' => $this->translate('Command'),
+ 'timePeriod' => $this->translate('Time Period'),
+ 'notification' => $this->translate('Notification'),
+ 'scheduledDowntime' => $this->translate('Scheduled Downtime'),
+ 'dependency' => $this->translate('Dependency'),
+ 'endpoint' => $this->translate('Endpoint'),
+ 'zone' => $this->translate('Zone'),
+ ];
+
+ $this->addElement('text', 'rule_name', [
+ 'label' => $this->translate('Rule name'),
+ 'description' => $this->translate('Please provide a rule name'),
+ 'required' => true,
+ ]);
+
+ $this->addElement('textarea', 'description', [
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'An extended description for this Sync Rule. This should explain'
+ . ' what this Rule is going to accomplish.'
+ ),
+ 'rows' => '3',
+ ]);
+
+ $this->addElement('select', 'object_type', [
+ 'label' => $this->translate('Object Type'),
+ 'description' => $this->translate('Choose an object type'),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum($availableTypes)
+ ]);
+
+ $this->addElement('select', 'update_policy', [
+ 'label' => $this->translate('Update Policy'),
+ 'description' => $this->translate(
+ 'Define what should happen when an object with a matching key'
+ . " already exists. You could merge its properties (import source"
+ . ' wins), replace it completely with the imported object or ignore'
+ . ' it (helpful for one-time imports). "Update only" means that this'
+ . ' Rule would never create (or delete) full Objects.'
+ ),
+ 'required' => true,
+ 'multiOptions' => $this->optionalEnum([
+ 'merge' => $this->translate('Merge'),
+ 'override' => $this->translate('Replace'),
+ 'ignore' => $this->translate('Ignore'),
+ 'update-only' => $this->translate('Update only'),
+ ])
+ ]);
+
+ $this->addBoolean('purge_existing', [
+ 'label' => $this->translate('Purge'),
+ 'description' => $this->translate(
+ 'Whether to purge existing objects. This means that objects of'
+ . ' the same type will be removed from Director in case they no'
+ . ' longer exist at your import source.'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ ]);
+
+ if ($this->getSentOrObjectValue('purge_existing') === 'y') {
+ $this->addElement('select', 'purge_action', [
+ 'label' => $this->translate('Purge Action'),
+ 'description' => $this->translate(
+ 'Whether to delete or to disable objects subject to purge'
+ ),
+ 'multiOptions' => $this->optionalEnum([
+ 'delete' => $this->translate('Delete'),
+ 'disable' => $this->translate('Disable'),
+ ]),
+ 'required' => true,
+ ]);
+ }
+
+ $this->addElement('text', 'filter_expression', [
+ 'label' => $this->translate('Filter Expression'),
+ 'description' => sprintf(
+ $this->translate(
+ 'Sync only part of your imported objects with this rule. Icinga Web 2'
+ . ' filter syntax is allowed, so this could look as follows: %s'
+ ),
+ '(host=a|host=b)&!ip=127.*'
+ ) . ' ' . $this->translate(
+ 'Be careful: this is usually NOT what you want, as it makes Sync "blind"'
+ . ' for objects matching this filter. This means that "Purge" will not'
+ . ' work as expected. The "Black/Whitelist" Import Property Modifier'
+ . ' is probably what you\'re looking for.'
+ ),
+ ]);
+
+ $this->setButtons();
+ }
+}
diff --git a/application/forms/SyncRunForm.php b/application/forms/SyncRunForm.php
new file mode 100644
index 0000000..0bc5fda
--- /dev/null
+++ b/application/forms/SyncRunForm.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Forms;
+
+use gipfl\Translation\TranslationHelper;
+use gipfl\Web\Form;
+use Icinga\Module\Director\Data\Db\DbObjectStore;
+use Icinga\Module\Director\Import\Sync;
+use Icinga\Module\Director\Objects\SyncRule;
+
+class SyncRunForm extends Form
+{
+ use TranslationHelper;
+
+ protected $defaultDecoratorClass = null;
+
+ /** @var ?string */
+ protected $successMessage = null;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ /** @var DbObjectStore */
+ protected $store;
+
+ public function __construct(SyncRule $rule, DbObjectStore $store)
+ {
+ $this->rule = $rule;
+ $this->store = $store;
+ }
+
+ public function assemble()
+ {
+ if ($this->store->getBranch()->isBranch()) {
+ $label = sprintf($this->translate('Sync to Branch: %s'), $this->store->getBranch()->getName());
+ } else {
+ $label = $this->translate('Trigger this Sync');
+ }
+ $this->addElement('submit', 'submit', [
+ 'label' => $label,
+ ]);
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSuccessMessage()
+ {
+ return $this->successMessage;
+ }
+
+ public function onSuccess()
+ {
+ $sync = new Sync($this->rule, $this->store);
+ if ($sync->hasModifications()) {
+ if ($sync->apply()) {
+ // and changed
+ $this->successMessage = $this->translate(('Source has successfully been synchronized'));
+ } else {
+ $this->successMessage = $this->translate('Nothing changed, rule is in sync');
+ }
+ } else {
+ // Used to be $rule->get('sync_state') === 'in-sync', $changed = $rule->applyChanges();
+ $this->successMessage = $this->translate('Nothing to do, rule is in sync');
+ }
+ }
+}