diff options
Diffstat (limited to '')
21 files changed, 2862 insertions, 0 deletions
diff --git a/application/forms/ApiTransportForm.php b/application/forms/ApiTransportForm.php new file mode 100644 index 0000000..27c147b --- /dev/null +++ b/application/forms/ApiTransportForm.php @@ -0,0 +1,102 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms; + +use Icinga\Data\ConfigObject; +use Icinga\Module\Icingadb\Command\Transport\CommandTransport; +use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; +use Icinga\Web\Session; +use ipl\Web\Common\CsrfCounterMeasure; +use ipl\Web\Compat\CompatForm; + +class ApiTransportForm extends CompatForm +{ + use CsrfCounterMeasure; + + protected function assemble() + { + // TODO: Use a validator to check if a name is not already in use + $this->addElement('text', 'name', [ + 'required' => true, + 'label' => t('Transport Name') + ]); + + $this->addElement('hidden', 'transport', [ + 'value' => 'api' + ]); + + $this->addElement('text', 'host', [ + 'required' => true, + 'id' => 'api_transport_host', + 'label' => t('Host'), + 'description' => t('Hostname or address of the Icinga master') + ]); + + // TODO: Don't rely only on browser validation + $this->addElement('number', 'port', [ + 'required' => true, + 'label' => t('Port'), + 'value' => 5665, + 'min' => 1, + 'max' => 65536 + ]); + + $this->addElement('text', 'username', [ + 'required' => true, + 'label' => t('API Username'), + 'description' => t('User to authenticate with using HTTP Basic Auth') + ]); + + $this->addElement('password', 'password', [ + 'required' => true, + 'autocomplete' => 'new-password', + 'label' => t('API Password') + ]); + + $this->addElement('submit', 'btn_submit', [ + 'label' => t('Save') + ]); + + $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + } + + public function validate() + { + parent::validate(); + if (! $this->isValid) { + return $this; + } + + if ($this->getPopulatedValue('force_creation') === 'y') { + return $this; + } + + try { + CommandTransport::createTransport(new ConfigObject($this->getValues()))->probe(); + } catch (CommandTransportException $e) { + $this->addMessage( + sprintf(t('Failed to successfully validate the configuration: %s'), $e->getMessage()) + ); + + $forceCheckbox = $this->createElement( + 'checkbox', + 'force_creation', + [ + 'ignore' => true, + 'label' => t('Force Changes'), + 'description' => t('Check this box to enforce changes without connectivity validation') + ] + ); + + $this->registerElement($forceCheckbox); + $this->decorate($forceCheckbox); + $this->prepend($forceCheckbox); + + $this->isValid = false; + } + + return $this; + } +} diff --git a/application/forms/Command/CommandForm.php b/application/forms/Command/CommandForm.php new file mode 100644 index 0000000..a535c6d --- /dev/null +++ b/application/forms/Command/CommandForm.php @@ -0,0 +1,179 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command; + +use ArrayIterator; +use Exception; +use Generator; +use Icinga\Application\Logger; +use Icinga\Module\Icingadb\Command\IcingaCommand; +use Icinga\Module\Icingadb\Command\Transport\CommandTransport; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Web\Notification; +use Icinga\Web\Session; +use ipl\Html\Form; +use ipl\Orm\Model; +use ipl\Web\Common\CsrfCounterMeasure; +use Traversable; + +abstract class CommandForm extends Form +{ + use Auth; + use CsrfCounterMeasure; + + protected $defaultAttributes = ['class' => 'icinga-form icinga-controls']; + + /** @var mixed */ + protected $objects; + + /** @var bool */ + protected $isApiTarget = false; + + /** + * Whether an error occurred while sending the command + * + * Prevents the success message from being rendered simultaneously + * + * @var bool + */ + protected $errorOccurred = false; + + /** + * Set the objects to issue the command for + * + * @param mixed $objects A traversable that is also countable + * + * @return $this + */ + public function setObjects($objects): self + { + $this->objects = $objects; + + return $this; + } + + /** + * Get the objects to issue the command for + * + * @return mixed + */ + public function getObjects() + { + return $this->objects; + } + + /** + * Set whether this form is an API target + * + * @param bool $state + * + * @return $this + */ + public function setIsApiTarget(bool $state = true): self + { + $this->isApiTarget = $state; + + return $this; + } + + /** + * Get whether this form is an API target + * + * @return bool + */ + public function isApiTarget(): bool + { + return $this->isApiTarget; + } + + /** + * Create and add form elements representing the command's options + * + * @return void + */ + abstract protected function assembleElements(); + + /** + * Create and add a submit button to the form + * + * @return void + */ + abstract protected function assembleSubmitButton(); + + /** + * Get the commands to issue for the given objects + * + * @param Traversable<Model> $objects + * + * @return Traversable<IcingaCommand> + */ + abstract protected function getCommands(Traversable $objects): Traversable; + + protected function assemble() + { + $this->assembleElements(); + + if (! $this->isApiTarget()) { + $this->assembleSubmitButton(); + $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + } + } + + protected function onSuccess() + { + $errors = []; + $objects = $this->getObjects(); + + foreach ($this->getCommands(is_array($objects) ? new ArrayIterator($objects) : $objects) as $command) { + try { + $this->sendCommand($command); + } catch (Exception $e) { + Logger::error($e->getMessage()); + $errors[] = $e->getMessage(); + } + } + + if (! empty($errors)) { + if (count($errors) > 1) { + Notification::warning( + t('Some commands were not transmitted. Please check the log. The first error follows.') + ); + } + + $this->errorOccurred = true; + + Notification::error($errors[0]); + } + } + + /** + * Transmit the given command + * + * @param IcingaCommand $command + * + * @return void + */ + protected function sendCommand(IcingaCommand $command) + { + (new CommandTransport())->send($command); + } + + /** + * Yield the $objects the currently logged in user has the permission $permission for + * + * @param string $permission + * @param Traversable $objects + * + * @return Generator + */ + protected function filterGrantedOn(string $permission, Traversable $objects): Generator + { + foreach ($objects as $object) { + if ($this->isGrantedOn($permission, $object)) { + yield $object; + } + } + } +} diff --git a/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php b/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php new file mode 100644 index 0000000..cf14db8 --- /dev/null +++ b/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php @@ -0,0 +1,154 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Instance; + +use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use Traversable; + +class ToggleInstanceFeaturesForm extends CommandForm +{ + protected $features; + + protected $featureStatus; + + /** + * ToggleFeature(s) being used to submit this form + * + * @var ToggleInstanceFeatureCommand[] + */ + protected $submittedFeatures = []; + + public function __construct(array $featureStatus) + { + $this->featureStatus = $featureStatus; + $this->features = [ + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS => + t('Active Host Checks'), + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS => + t('Active Service Checks'), + ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS => + t('Event Handlers'), + ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION => + t('Flap Detection'), + ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS => + t('Notifications'), + ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA => + t('Performance Data') + ]; + + $this->getAttributes()->add('class', 'instance-features'); + + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + foreach ($this->submittedFeatures as $feature) { + $enabled = $feature->getEnabled(); + switch ($feature->getFeature()) { + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: + if ($enabled) { + $message = t('Enabled active host checks successfully'); + } else { + $message = t('Disabled active host checks successfully'); + } + + break; + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: + if ($enabled) { + $message = t('Enabled active service checks successfully'); + } else { + $message = t('Disabled active service checks successfully'); + } + + break; + case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: + if ($enabled) { + $message = t('Enabled event handlers successfully'); + } else { + $message = t('Disabled event handlers checks successfully'); + } + + break; + case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: + if ($enabled) { + $message = t('Enabled flap detection successfully'); + } else { + $message = t('Disabled flap detection successfully'); + } + + break; + case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: + if ($enabled) { + $message = t('Enabled notifications successfully'); + } else { + $message = t('Disabled notifications successfully'); + } + + break; + case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: + if ($enabled) { + $message = t('Enabled performance data successfully'); + } else { + $message = t('Disabled performance data successfully'); + } + + break; + default: + $message = t('Invalid feature option'); + break; + } + + Notification::success($message); + } + }); + } + + protected function assembleElements() + { + $disabled = ! $this->getAuth()->hasPermission('icingadb/command/feature/instance'); + $decorator = new IcingaFormDecorator(); + + foreach ($this->features as $feature => $label) { + $this->addElement( + 'checkbox', + $feature, + [ + 'class' => 'autosubmit', + 'label' => $label, + 'disabled' => $disabled, + 'value' => (bool) $this->featureStatus[$feature] + ] + ); + $decorator->decorate($this->getElement($feature)); + } + } + + protected function assembleSubmitButton() + { + } + + protected function getCommands(Traversable $objects): Traversable + { + foreach ($this->features as $feature => $spec) { + $featureState = $this->getElement($feature)->isChecked(); + + if ((int) $featureState === (int) $this->featureStatus[$feature]) { + continue; + } + + $command = new ToggleInstanceFeatureCommand(); + $command->setFeature($feature); + $command->setEnabled($featureState); + + $this->submittedFeatures[] = $command; + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/AcknowledgeProblemForm.php b/application/forms/Command/Object/AcknowledgeProblemForm.php new file mode 100644 index 0000000..81b93e2 --- /dev/null +++ b/application/forms/Command/Object/AcknowledgeProblemForm.php @@ -0,0 +1,210 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Icinga\Application\Config; +use Icinga\Module\Icingadb\Command\Object\AcknowledgeProblemCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Validator\CallbackValidator; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use ipl\Web\Widget\Icon; +use Traversable; + +use function ipl\Stdlib\iterable_value_first; + +class AcknowledgeProblemForm extends CommandForm +{ + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + if (iterable_value_first($this->getObjects()) instanceof Host) { + $message = sprintf(tp( + 'Acknowledged problem successfully', + 'Acknowledged problem on %d hosts successfully', + $countObjects + ), $countObjects); + } else { + $message = sprintf(tp( + 'Acknowledged problem successfully', + 'Acknowledged problem on %d services successfully', + $countObjects + ), $countObjects); + } + + Notification::success($message); + }); + } + + protected function assembleElements() + { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'form-description']), + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement( + 'ul', + null, + new HtmlElement('li', null, Text::create(t( + 'This command is used to acknowledge host or service problems. When a problem is acknowledged,' + . ' future notifications about problems are temporarily disabled until the host or service' + . ' recovers.' + ))) + ) + )); + + $config = Config::module('icingadb'); + $decorator = new IcingaFormDecorator(); + + $this->addElement( + 'textarea', + 'comment', + [ + 'required' => true, + 'label' => t('Comment'), + 'description' => t( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ) + ] + ); + $decorator->decorate($this->getElement('comment')); + + $this->addElement( + 'checkbox', + 'persistent', + [ + 'label' => t('Persistent Comment'), + 'value' => (bool) $config->get('settings', 'acknowledge_persistent', false), + 'description' => t( + 'If you want the comment to remain even when the acknowledgement is removed, check this' + . ' option.' + ) + ] + ); + $decorator->decorate($this->getElement('persistent')); + + $this->addElement( + 'checkbox', + 'notify', + [ + 'label' => t('Send Notification'), + 'value' => (bool) $config->get('settings', 'acknowledge_notify', true), + 'description' => t( + 'If you want an acknowledgement notification to be sent out to the appropriate contacts,' + . ' check this option.' + ) + ] + ); + $decorator->decorate($this->getElement('notify')); + + $this->addElement( + 'checkbox', + 'sticky', + [ + 'label' => t('Sticky Acknowledgement'), + 'value' => (bool) $config->get('settings', 'acknowledge_sticky', false), + 'description' => t( + 'If you want the acknowledgement to remain until the host or service recovers even if the host' + . ' or service changes state, check this option.' + ) + ] + ); + $decorator->decorate($this->getElement('sticky')); + + $this->addElement( + 'checkbox', + 'expire', + [ + 'ignore' => true, + 'class' => 'autosubmit', + 'value' => (bool) $config->get('settings', 'acknowledge_expire', false), + 'label' => t('Use Expire Time'), + 'description' => t('If the acknowledgement should expire, check this option.') + ] + ); + $decorator->decorate($this->getElement('expire')); + + if ($this->getElement('expire')->isChecked()) { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval($config->get('settings', 'acknowledge_expire_time', 'PT1H'))); + + $this->addElement( + 'localDateTime', + 'expire_time', + [ + 'data-use-datetime-picker' => true, + 'required' => true, + 'value' => $expireTime, + 'label' => t('Expire Time'), + 'description' => t( + 'Choose the date and time when Icinga should delete the acknowledgement.' + ), + 'validators' => [ + 'DateTime' => ['break_chain_on_failure' => true], + 'Callback' => function ($value, $validator) { + /** @var CallbackValidator $validator */ + if ($value <= (new DateTime())) { + $validator->addMessage(t('The expire time must not be in the past')); + return false; + } + + return true; + } + ] + ] + ); + $decorator->decorate($this->getElement('expire_time')); + } + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submit', + 'btn_submit', + [ + 'required' => true, + 'label' => tp('Acknowledge problem', 'Acknowledge problems', count($this->getObjects())) + ] + ); + + (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = $this->filterGrantedOn('icingadb/command/acknowledge-problem', $objects); + + if ($granted->valid()) { + $command = new AcknowledgeProblemCommand(); + $command->setObjects($granted); + $command->setComment($this->getValue('comment')); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + $command->setNotify($this->getElement('notify')->isChecked()); + $command->setSticky($this->getElement('sticky')->isChecked()); + $command->setPersistent($this->getElement('persistent')->isChecked()); + + if (($expireTime = $this->getValue('expire_time')) !== null) { + /** @var DateTime $expireTime */ + $command->setExpireTime($expireTime->getTimestamp()); + } + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/AddCommentForm.php b/application/forms/Command/Object/AddCommentForm.php new file mode 100644 index 0000000..9cd0754 --- /dev/null +++ b/application/forms/Command/Object/AddCommentForm.php @@ -0,0 +1,162 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Icinga\Application\Config; +use Icinga\Module\Icingadb\Command\Object\AddCommentCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Validator\CallbackValidator; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use ipl\Web\Widget\Icon; +use Traversable; + +use function ipl\Stdlib\iterable_value_first; + +class AddCommentForm extends CommandForm +{ + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + if (iterable_value_first($this->getObjects()) instanceof Host) { + $message = sprintf( + tp('Added comment successfully', 'Added comment to %d hosts successfully', $countObjects), + $countObjects + ); + } else { + $message = sprintf( + tp('Added comment successfully', 'Added comment to %d services successfully', $countObjects), + $countObjects + ); + } + + Notification::success($message); + }); + } + + protected function assembleElements() + { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'form-description']), + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement( + 'ul', + null, + new HtmlElement( + 'li', + null, + Text::create(t('This command is used to add host or service comments.')) + ) + ) + )); + + $decorator = new IcingaFormDecorator(); + + $this->addElement( + 'textarea', + 'comment', + [ + 'required' => true, + 'label' => t('Comment'), + 'description' => t( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ) + ] + ); + $decorator->decorate($this->getElement('comment')); + + $config = Config::module('icingadb'); + + $this->addElement( + 'checkbox', + 'expire', + [ + 'ignore' => true, + 'class' => 'autosubmit', + 'value' => (bool) $config->get('settings', 'comment_expire', false), + 'label' => t('Use Expire Time'), + 'description' => t('If the comment should expire, check this option.') + ] + ); + $decorator->decorate($this->getElement('expire')); + + if ($this->getElement('expire')->isChecked()) { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval($config->get('settings', 'comment_expire_time', 'PT1H'))); + + $this->addElement( + 'localDateTime', + 'expire_time', + [ + 'data-use-datetime-picker' => true, + 'required' => true, + 'value' => $expireTime, + 'label' => t('Expire Time'), + 'description' => t('Choose the date and time when Icinga should delete the comment.'), + 'validators' => [ + 'DateTime' => ['break_chain_on_failure' => true], + 'Callback' => function ($value, $validator) { + /** @var CallbackValidator $validator */ + if ($value <= (new DateTime())) { + $validator->addMessage(t('The expire time must not be in the past')); + return false; + } + + return true; + } + ] + ] + ); + $decorator->decorate($this->getElement('expire_time')); + } + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submit', + 'btn_submit', + [ + 'required' => true, + 'label' => tp('Add comment', 'Add comments', count($this->getObjects())) + ] + ); + + (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = $this->filterGrantedOn('icingadb/command/comment/add', $objects); + + if ($granted->valid()) { + $command = new AddCommentCommand(); + $command->setObjects($granted); + $command->setComment($this->getValue('comment')); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + if (($expireTime = $this->getValue('expire_time'))) { + /** @var DateTime $expireTime */ + $command->setExpireTime($expireTime->getTimestamp()); + } + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/CheckNowForm.php b/application/forms/Command/Object/CheckNowForm.php new file mode 100644 index 0000000..b7a506c --- /dev/null +++ b/application/forms/Command/Object/CheckNowForm.php @@ -0,0 +1,72 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Generator; +use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use ipl\Web\Widget\Icon; +use Traversable; + +class CheckNowForm extends CommandForm +{ + protected $defaultAttributes = ['class' => 'inline']; + + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if (! $this->errorOccurred) { + Notification::success(tp('Scheduling check..', 'Scheduling checks..', count($this->getObjects()))); + } + }); + } + + protected function assembleElements() + { + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submitButton', + 'btn_submit', + [ + 'class' => ['link-button', 'spinner'], + 'label' => [ + new Icon('sync-alt'), + t('Check Now') + ], + 'title' => t('Schedule the next active check to run immediately') + ] + ); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = (function () use ($objects): Generator { + foreach ($objects as $object) { + if ( + $this->isGrantedOn('icingadb/command/schedule-check', $object) + || ( + $object->active_checks_enabled + && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $object) + ) + ) { + yield $object; + } + } + })(); + + if ($granted->valid()) { + $command = new ScheduleCheckCommand(); + $command->setObjects($granted); + $command->setCheckTime(time()); + $command->setForced(); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/DeleteCommentForm.php b/application/forms/Command/Object/DeleteCommentForm.php new file mode 100644 index 0000000..25275ba --- /dev/null +++ b/application/forms/Command/Object/DeleteCommentForm.php @@ -0,0 +1,75 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Generator; +use Icinga\Module\Icingadb\Command\Object\DeleteCommentCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use ipl\Web\Common\RedirectOption; +use ipl\Web\Widget\Icon; +use Traversable; + +class DeleteCommentForm extends CommandForm +{ + use RedirectOption; + + protected $defaultAttributes = ['class' => 'inline']; + + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + + Notification::success(sprintf( + tp('Removed comment successfully', 'Removed comment from %d objects successfully', $countObjects), + $countObjects + )); + }); + } + + protected function assembleElements() + { + $this->addElement($this->createRedirectOption()); + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submitButton', + 'btn_submit', + [ + 'class' => ['cancel-button', 'spinner'], + 'label' => [ + new Icon('trash'), + tp('Remove Comment', 'Remove Comments', count($this->getObjects())) + ] + ] + ); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = (function () use ($objects): Generator { + foreach ($objects as $object) { + if ($this->isGrantedOn('icingadb/command/comment/delete', $object->{$object->object_type})) { + yield $object; + } + } + })(); + + if ($granted->valid()) { + $command = new DeleteCommentCommand(); + $command->setObjects($granted); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/DeleteDowntimeForm.php b/application/forms/Command/Object/DeleteDowntimeForm.php new file mode 100644 index 0000000..5f695b9 --- /dev/null +++ b/application/forms/Command/Object/DeleteDowntimeForm.php @@ -0,0 +1,90 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Generator; +use Icinga\Module\Icingadb\Command\Object\DeleteDowntimeCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use ipl\Web\Common\RedirectOption; +use ipl\Web\Widget\Icon; +use Traversable; + +class DeleteDowntimeForm extends CommandForm +{ + use RedirectOption; + + protected $defaultAttributes = ['class' => 'inline']; + + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + + Notification::success(sprintf( + tp('Removed downtime successfully', 'Removed downtime from %d objects successfully', $countObjects), + $countObjects + )); + }); + } + + protected function assembleElements() + { + $this->addElement($this->createRedirectOption()); + } + + protected function assembleSubmitButton() + { + $isDisabled = true; + foreach ($this->getObjects() as $downtime) { + if ($downtime->scheduled_by === null) { + $isDisabled = false; + break; + } + } + + $this->addElement( + 'submitButton', + 'btn_submit', + [ + 'class' => ['cancel-button', 'spinner'], + 'disabled' => $isDisabled ?: null, + 'title' => $isDisabled + ? t('Downtime cannot be removed at runtime because it is based on a configured scheduled downtime.') + : null, + 'label' => [ + new Icon('trash'), + tp('Delete downtime', 'Delete downtimes', count($this->getObjects())) + ] + ] + ); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = (function () use ($objects): Generator { + foreach ($objects as $object) { + if ( + $this->isGrantedOn('icingadb/command/downtime/delete', $object->{$object->object_type}) + && $object->scheduled_by === null + ) { + yield $object; + } + } + })(); + + if ($granted->valid()) { + $command = new DeleteDowntimeCommand(); + $command->setObjects($granted); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/ProcessCheckResultForm.php b/application/forms/Command/Object/ProcessCheckResultForm.php new file mode 100644 index 0000000..5764bf8 --- /dev/null +++ b/application/forms/Command/Object/ProcessCheckResultForm.php @@ -0,0 +1,156 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Generator; +use Icinga\Module\Icingadb\Command\Object\ProcessCheckResultCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Orm\Model; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use ipl\Web\Widget\Icon; +use Traversable; + +use function ipl\Stdlib\iterable_value_first; + +class ProcessCheckResultForm extends CommandForm +{ + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + if (iterable_value_first($this->getObjects()) instanceof Host) { + $message = sprintf(tp( + 'Submitted passive check result successfully', + 'Submitted passive check result for %d hosts successfully', + $countObjects + ), $countObjects); + } else { + $message = sprintf(tp( + 'Submitted passive check result successfully', + 'Submitted passive check result for %d services successfully', + $countObjects + ), $countObjects); + } + + Notification::success($message); + }); + } + + protected function assembleElements() + { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'form-description']), + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement( + 'ul', + null, + new HtmlElement( + 'li', + null, + Text::create(t('This command is used to submit passive host or service check results.')) + ) + ) + )); + + $decorator = new IcingaFormDecorator(); + + /** @var Model $object */ + $object = iterable_value_first($this->getObjects()); + + $this->addElement( + 'select', + 'status', + [ + 'required' => true, + 'label' => t('Status'), + 'description' => t('The state this check result should report'), + 'options' => $object instanceof Host ? [ + ProcessCheckResultCommand::HOST_UP => t('UP', 'icinga.state'), + ProcessCheckResultCommand::HOST_DOWN => t('DOWN', 'icinga.state') + ] : [ + ProcessCheckResultCommand::SERVICE_OK => t('OK', 'icinga.state'), + ProcessCheckResultCommand::SERVICE_WARNING => t('WARNING', 'icinga.state'), + ProcessCheckResultCommand::SERVICE_CRITICAL => t('CRITICAL', 'icinga.state'), + ProcessCheckResultCommand::SERVICE_UNKNOWN => t('UNKNOWN', 'icinga.state') + ] + ] + ); + $decorator->decorate($this->getElement('status')); + + $this->addElement( + 'text', + 'output', + [ + 'required' => true, + 'label' => t('Output'), + 'description' => t('The plugin output of this check result') + ] + ); + $decorator->decorate($this->getElement('output')); + + $this->addElement( + 'text', + 'perfdata', + [ + 'allowEmpty' => true, + 'label' => t('Performance Data'), + 'description' => t( + 'The performance data of this check result. Leave empty' + . ' if this check result has no performance data' + ) + ] + ); + $decorator->decorate($this->getElement('perfdata')); + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submit', + 'btn_submit', + [ + 'required' => true, + 'label' => tp( + 'Submit Passive Check Result', + 'Submit Passive Check Results', + count($this->getObjects()) + ) + ] + ); + + (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = (function () use ($objects): Generator { + foreach ($this->filterGrantedOn('icingadb/command/process-check-result', $objects) as $object) { + if ($object->passive_checks_enabled) { + yield $object; + } + } + })(); + + if ($granted->valid()) { + $command = new ProcessCheckResultCommand(); + $command->setObjects($granted); + $command->setStatus($this->getValue('status')); + $command->setOutput($this->getValue('output')); + $command->setPerformanceData($this->getValue('perfdata')); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/RemoveAcknowledgementForm.php b/application/forms/Command/Object/RemoveAcknowledgementForm.php new file mode 100644 index 0000000..8697985 --- /dev/null +++ b/application/forms/Command/Object/RemoveAcknowledgementForm.php @@ -0,0 +1,77 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Icinga\Module\Icingadb\Command\Object\RemoveAcknowledgementCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Web\Notification; +use ipl\Web\Widget\Icon; +use Traversable; + +use function ipl\Stdlib\iterable_value_first; + +class RemoveAcknowledgementForm extends CommandForm +{ + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + if (iterable_value_first($this->getObjects()) instanceof Host) { + $message = sprintf(tp( + 'Removed acknowledgment successfully', + 'Removed acknowledgment from %d hosts successfully', + $countObjects + ), $countObjects); + } else { + $message = sprintf(tp( + 'Removed acknowledgment successfully', + 'Removed acknowledgment from %d services successfully', + $countObjects + ), $countObjects); + } + + Notification::success($message); + }); + } + + protected $defaultAttributes = ['class' => 'inline']; + + protected function assembleElements() + { + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submitButton', + 'btn_submit', + [ + 'class' => ['link-button', 'spinner'], + 'label' => [ + new Icon('trash'), + tp('Remove acknowledgement', 'Remove acknowledgements', count($this->getObjects())) + ] + ] + ); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = $this->filterGrantedOn('icingadb/command/remove-acknowledgement', $objects); + + if ($granted->valid()) { + $command = new RemoveAcknowledgementCommand(); + $command->setObjects($granted); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/ScheduleCheckForm.php b/application/forms/Command/Object/ScheduleCheckForm.php new file mode 100644 index 0000000..9b32ea1 --- /dev/null +++ b/application/forms/Command/Object/ScheduleCheckForm.php @@ -0,0 +1,137 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Generator; +use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use ipl\Web\Widget\Icon; +use Traversable; + +use function ipl\Stdlib\iterable_value_first; + +class ScheduleCheckForm extends CommandForm +{ + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + if (iterable_value_first($this->getObjects()) instanceof Host) { + $message = sprintf( + tp('Scheduled check successfully', 'Scheduled check for %d hosts successfully', $countObjects), + $countObjects + ); + } else { + $message = sprintf( + tp('Scheduled check successfully', 'Scheduled check for %d services successfully', $countObjects), + $countObjects + ); + } + + Notification::success($message); + }); + } + + protected function assembleElements() + { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'form-description']), + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement( + 'ul', + null, + new HtmlElement( + 'li', + null, + Text::create(t( + 'This command is used to schedule the next check of hosts or services. Icinga' + . ' will re-queue the hosts or services to be checked at the time you specify.' + )) + ) + ) + )); + + $decorator = new IcingaFormDecorator(); + + $this->addElement( + 'localDateTime', + 'check_time', + [ + 'data-use-datetime-picker' => true, + 'required' => true, + 'label' => t('Check Time'), + 'description' => t('Set the date and time when the check should be scheduled.'), + 'value' => (new DateTime())->add(new DateInterval('PT1H')) + ] + ); + $decorator->decorate($this->getElement('check_time')); + + $this->addElement( + 'checkbox', + 'force_check', + [ + 'label' => t('Force Check'), + 'description' => t( + 'If you select this option, Icinga will force a check regardless of both what time the' + . ' scheduled check occurs and whether or not checks are enabled.' + ) + ] + ); + $decorator->decorate($this->getElement('force_check')); + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submit', + 'btn_submit', + [ + 'required' => true, + 'label' => tp('Schedule check', 'Schedule checks', count($this->getObjects())) + ] + ); + + (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = (function () use ($objects): Generator { + foreach ($objects as $object) { + if ( + $this->isGrantedOn('icingadb/command/schedule-check', $object) + || ( + $object->active_checks_enabled + && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $object) + ) + ) { + yield $object; + } + } + })(); + + if ($granted->valid()) { + $command = new ScheduleCheckCommand(); + $command->setObjects($granted); + $command->setForced($this->getElement('force_check')->isChecked()); + $command->setCheckTime($this->getValue('check_time')->getTimestamp()); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/ScheduleHostDowntimeForm.php b/application/forms/Command/Object/ScheduleHostDowntimeForm.php new file mode 100644 index 0000000..bc21114 --- /dev/null +++ b/application/forms/Command/Object/ScheduleHostDowntimeForm.php @@ -0,0 +1,119 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Icinga\Application\Config; +use Icinga\Module\Icingadb\Command\Object\PropagateHostDowntimeCommand; +use Icinga\Module\Icingadb\Command\Object\ScheduleHostDowntimeCommand; +use Icinga\Web\Notification; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use Traversable; + +class ScheduleHostDowntimeForm extends ScheduleServiceDowntimeForm +{ + /** @var bool */ + protected $hostDowntimeAllServices; + + public function __construct() + { + $this->start = new DateTime(); + $config = Config::module('icingadb'); + $this->commentText = $config->get('settings', 'hostdowntime_comment_text'); + + $this->hostDowntimeAllServices = (bool) $config->get('settings', 'hostdowntime_all_services', false); + + $fixedEnd = clone $this->start; + $fixed = $config->get('settings', 'hostdowntime_end_fixed', 'PT1H'); + $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed)); + + $flexibleEnd = clone $this->start; + $flexible = $config->get('settings', 'hostdowntime_end_flexible', 'PT2H'); + $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible)); + + $flexibleDuration = $config->get('settings', 'hostdowntime_flexible_duration', 'PT2H'); + $this->flexibleDuration = new DateInterval($flexibleDuration); + + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + + Notification::success(sprintf( + tp('Scheduled downtime successfully', 'Scheduled downtime for %d hosts successfully', $countObjects), + $countObjects + )); + }); + } + + protected function assembleElements() + { + parent::assembleElements(); + + $decorator = new IcingaFormDecorator(); + + $this->addElement( + 'checkbox', + 'all_services', + [ + 'label' => t('All Services'), + 'description' => t( + 'Sets downtime for all services for the matched host objects. If child options are set,' + . ' all child hosts and their services will schedule a downtime too.' + ), + 'value' => $this->hostDowntimeAllServices + ] + ); + $decorator->decorate($this->getElement('all_services')); + + $this->addElement( + 'select', + 'child_options', + array( + 'description' => t('Schedule child downtimes.'), + 'label' => t('Child Options'), + 'options' => [ + 0 => t('Do nothing with child hosts'), + 1 => t('Schedule triggered downtime for all child hosts'), + 2 => t('Schedule non-triggered downtime for all child hosts') + ] + ) + ); + $decorator->decorate($this->getElement('child_options')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = $this->filterGrantedOn('icingadb/command/downtime/schedule', $objects); + + if ($granted->valid()) { + if (($childOptions = (int) $this->getValue('child_options'))) { + $command = new PropagateHostDowntimeCommand(); + $command->setTriggered($childOptions === 1); + } else { + $command = new ScheduleHostDowntimeCommand(); + } + + $command->setObjects($granted); + $command->setComment($this->getValue('comment')); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + $command->setStart($this->getValue('start')->getTimestamp()); + $command->setEnd($this->getValue('end')->getTimestamp()); + $command->setForAllServices($this->getElement('all_services')->isChecked()); + + if ($this->getElement('flexible')->isChecked()) { + $command->setFixed(false); + $command->setDuration( + $this->getValue('hours') * 3600 + $this->getValue('minutes') * 60 + ); + } + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/ScheduleServiceDowntimeForm.php b/application/forms/Command/Object/ScheduleServiceDowntimeForm.php new file mode 100644 index 0000000..184a4e8 --- /dev/null +++ b/application/forms/Command/Object/ScheduleServiceDowntimeForm.php @@ -0,0 +1,267 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use DateInterval; +use DateTime; +use Icinga\Application\Config; +use Icinga\Module\Icingadb\Command\Object\ScheduleServiceDowntimeCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Validator\CallbackValidator; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use ipl\Web\Widget\Icon; +use Traversable; + +class ScheduleServiceDowntimeForm extends CommandForm +{ + /** @var DateTime downtime start */ + protected $start; + + /** @var DateTime fixed downtime end */ + protected $fixedEnd; + + /**@var DateTime flexible downtime end */ + protected $flexibleEnd; + + /** @var DateInterval flexible downtime duration */ + protected $flexibleDuration; + + /** @var mixed Comment Text */ + protected $commentText; + + /** + * Initialize this form + */ + public function __construct() + { + $this->start = new DateTime(); + + $config = Config::module('icingadb'); + + $this->commentText = $config->get('settings', 'hostdowntime_comment_text'); + $fixedEnd = clone $this->start; + $fixed = $config->get('settings', 'servicedowntime_end_fixed', 'PT1H'); + $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed)); + + $flexibleEnd = clone $this->start; + $flexible = $config->get('settings', 'servicedowntime_end_flexible', 'PT2H'); + $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible)); + + $flexibleDuration = $config->get('settings', 'servicedowntime_flexible_duration', 'PT2H'); + $this->flexibleDuration = new DateInterval($flexibleDuration); + + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + + Notification::success(sprintf( + tp('Scheduled downtime successfully', 'Scheduled downtime for %d services successfully', $countObjects), + $countObjects + )); + }); + } + + protected function assembleElements() + { + $isFlexible = $this->getPopulatedValue('flexible') === 'y'; + + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'form-description']), + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement( + 'ul', + null, + new HtmlElement( + 'li', + null, + Text::create(t( + 'This command is used to schedule host and service downtimes. During the downtime specified' + . ' by the start and end time, Icinga will not send notifications out about the hosts and' + . ' services. When the scheduled downtime expires, Icinga will send out notifications for' + . ' the hosts and services as it normally would.' + )) + ) + ) + )); + + $decorator = new IcingaFormDecorator(); + + $this->addElement( + 'textarea', + 'comment', + [ + 'required' => true, + 'label' => t('Comment'), + 'description' => t( + 'If you work with other administrators, you may find it useful to share information about' + . ' the host or service that is having problems. Make sure you enter a brief description of' + . ' what you are doing.' + ), + 'value' => $this->commentText + ] + ); + $decorator->decorate($this->getElement('comment')); + + $this->addElement( + 'localDateTime', + 'start', + [ + 'data-use-datetime-picker' => true, + 'required' => true, + 'value' => $this->start, + 'label' => t('Start Time'), + 'description' => t('Set the start date and time for the downtime.') + ] + ); + $decorator->decorate($this->getElement('start')); + + $this->addElement( + 'localDateTime', + 'end', + [ + 'data-use-datetime-picker' => true, + 'required' => true, + 'label' => t('End Time'), + 'description' => t('Set the end date and time for the downtime.'), + 'value' => $isFlexible ? $this->flexibleEnd : $this->fixedEnd, + 'validators' => [ + 'DateTime' => ['break_chain_on_failure' => true], + 'Callback' => function ($value, $validator) { + /** @var CallbackValidator $validator */ + + if ($value <= $this->getValue('start')) { + $validator->addMessage(t('The end time must be greater than the start time')); + return false; + } + + if ($value <= (new DateTime())) { + $validator->addMessage(t('A downtime must not be in the past')); + return false; + } + + return true; + } + ] + ] + ); + $decorator->decorate($this->getElement('end')); + + $this->addElement( + 'checkbox', + 'flexible', + [ + 'class' => 'autosubmit', + 'label' => t('Flexible'), + 'description' => t( + 'To make this a flexible downtime, check this option. A flexible downtime starts when the host' + . ' or service enters a problem state sometime between the start and end times you specified.' + . ' It then lasts as long as the duration time you enter.' + ) + ] + ); + $decorator->decorate($this->getElement('flexible')); + + if ($isFlexible) { + $hoursInput = $this->createElement( + 'number', + 'hours', + [ + 'required' => true, + 'label' => t('Duration'), + 'value' => $this->flexibleDuration->h, + 'min' => 0, + 'validators' => [ + 'Callback' => function ($value, $validator) { + /** @var CallbackValidator $validator */ + + if ($this->getValue('minutes') == 0 && $value == 0) { + $validator->addMessage(t('The duration must not be zero')); + return false; + } + + return true; + } + ] + ] + ); + $this->registerElement($hoursInput); + $decorator->decorate($hoursInput); + + $minutesInput = $this->createElement( + 'number', + 'minutes', + [ + 'required' => true, + 'value' => $this->flexibleDuration->m, + 'min' => 0 + ] + ); + $this->registerElement($minutesInput); + $minutesInput->addWrapper( + new HtmlElement('label', null, new HtmlElement('span', null, Text::create(t('Minutes')))) + ); + + $hoursInput->getWrapper()->on( + IcingaFormDecorator::ON_ASSEMBLED, + function ($hoursInputWrapper) use ($minutesInput, $hoursInput) { + $hoursInputWrapper + ->insertAfter($minutesInput, $hoursInput) + ->getAttributes()->add('class', 'downtime-duration'); + } + ); + + $hoursInput->prependWrapper( + new HtmlElement('label', null, new HtmlElement('span', null, Text::create(t('Hours')))) + ); + + $this->add($hoursInput); + } + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submit', + 'btn_submit', + [ + 'required' => true, + 'label' => tp('Schedule downtime', 'Schedule downtimes', count($this->getObjects())) + ] + ); + + (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = $this->filterGrantedOn('icingadb/command/downtime/schedule', $objects); + + if ($granted->valid()) { + $command = new ScheduleServiceDowntimeCommand(); + $command->setObjects($granted); + $command->setComment($this->getValue('comment')); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + $command->setStart($this->getValue('start')->getTimestamp()); + $command->setEnd($this->getValue('end')->getTimestamp()); + + if ($this->getElement('flexible')->isChecked()) { + $command->setFixed(false); + $command->setDuration( + $this->getValue('hours') * 3600 + $this->getValue('minutes') * 60 + ); + } + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/SendCustomNotificationForm.php b/application/forms/Command/Object/SendCustomNotificationForm.php new file mode 100644 index 0000000..dfb1e96 --- /dev/null +++ b/application/forms/Command/Object/SendCustomNotificationForm.php @@ -0,0 +1,125 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Icinga\Application\Config; +use Icinga\Module\Icingadb\Command\Object\SendCustomNotificationCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Web\Notification; +use ipl\Html\Attributes; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use ipl\Web\Widget\Icon; +use Traversable; + +use function ipl\Stdlib\iterable_value_first; + +class SendCustomNotificationForm extends CommandForm +{ + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + $countObjects = count($this->getObjects()); + if (iterable_value_first($this->getObjects()) instanceof Host) { + $message = sprintf(tp( + 'Sent custom notification successfully', + 'Sent custom notification for %d hosts successfully', + $countObjects + ), $countObjects); + } else { + $message = sprintf(tp( + 'Sent custom notification successfully', + 'Sent custom notification for %d services successfully', + $countObjects + ), $countObjects); + } + + Notification::success($message); + }); + } + + protected function assembleElements() + { + $this->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'form-description']), + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement( + 'ul', + null, + new HtmlElement( + 'li', + null, + Text::create(t('This command is used to send custom notifications about hosts or services.')) + ) + ) + )); + + $config = Config::module('icingadb'); + $decorator = new IcingaFormDecorator(); + + $this->addElement( + 'textarea', + 'comment', + [ + 'required' => true, + 'label' => t('Comment'), + 'description' => t( + 'Enter a brief description on why you\'re sending this notification. It will be sent with it.' + ) + ] + ); + $decorator->decorate($this->getElement('comment')); + + $this->addElement( + 'checkbox', + 'forced', + [ + 'label' => t('Forced'), + 'value' => (bool) $config->get('settings', 'custom_notification_forced', false), + 'description' => t( + 'If you check this option, the notification is sent regardless' + . ' of downtimes or whether notifications are enabled or not.' + ) + ] + ); + $decorator->decorate($this->getElement('forced')); + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submit', + 'btn_submit', + [ + 'required' => true, + 'label' => tp('Send custom notification', 'Send custom notifications', count($this->getObjects())) + ] + ); + + (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); + } + + protected function getCommands(Traversable $objects): Traversable + { + $granted = $this->filterGrantedOn('icingadb/command/send-custom-notification', $objects); + + if ($granted->valid()) { + $command = new SendCustomNotificationCommand(); + $command->setObjects($granted); + $command->setComment($this->getValue('comment')); + $command->setForced($this->getElement('forced')->isChecked()); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/ToggleObjectFeaturesForm.php b/application/forms/Command/Object/ToggleObjectFeaturesForm.php new file mode 100644 index 0000000..50767da --- /dev/null +++ b/application/forms/Command/Object/ToggleObjectFeaturesForm.php @@ -0,0 +1,186 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Command\Object; + +use Icinga\Module\Icingadb\Command\Object\ToggleObjectFeatureCommand; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use ipl\Html\FormElement\CheckboxElement; +use ipl\Orm\Model; +use ipl\Web\FormDecorator\IcingaFormDecorator; +use Traversable; + +class ToggleObjectFeaturesForm extends CommandForm +{ + const LEAVE_UNCHANGED = 'noop'; + + protected $features; + + protected $featureStatus; + + /** + * ToggleFeature(s) being used to submit this form + * + * @var ToggleObjectFeatureCommand[] + */ + protected $submittedFeatures = []; + + public function __construct($featureStatus) + { + $this->featureStatus = $featureStatus; + $this->features = [ + ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => [ + 'label' => t('Active Checks'), + 'permission' => 'icingadb/command/feature/object/active-checks' + ], + ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => [ + 'label' => t('Passive Checks'), + 'permission' => 'icingadb/command/feature/object/passive-checks' + ], + ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => [ + 'label' => t('Notifications'), + 'permission' => 'icingadb/command/feature/object/notifications' + ], + ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => [ + 'label' => t('Event Handler'), + 'permission' => 'icingadb/command/feature/object/event-handler' + ], + ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => [ + 'label' => t('Flap Detection'), + 'permission' => 'icingadb/command/feature/object/flap-detection' + ] + ]; + + $this->getAttributes()->add('class', 'object-features'); + + $this->on(self::ON_SUCCESS, function () { + if ($this->errorOccurred) { + return; + } + + foreach ($this->submittedFeatures as $feature) { + $enabled = $feature->getEnabled(); + switch ($feature->getFeature()) { + case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS: + if ($enabled) { + $message = t('Enabled active checks successfully'); + } else { + $message = t('Disabled active checks successfully'); + } + + break; + case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS: + if ($enabled) { + $message = t('Enabled passive checks successfully'); + } else { + $message = t('Disabled passive checks successfully'); + } + + break; + case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER: + if ($enabled) { + $message = t('Enabled event handler successfully'); + } else { + $message = t('Disabled event handler checks successfully'); + } + + break; + case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION: + if ($enabled) { + $message = t('Enabled flap detection successfully'); + } else { + $message = t('Disabled flap detection successfully'); + } + + break; + case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS: + if ($enabled) { + $message = t('Enabled notifications successfully'); + } else { + $message = t('Disabled notifications successfully'); + } + + break; + default: + $message = t('Invalid feature option'); + break; + } + + Notification::success($message); + } + }); + } + + protected function assembleElements() + { + $decorator = new IcingaFormDecorator(); + foreach ($this->features as $feature => $spec) { + $options = [ + 'class' => 'autosubmit', + 'disabled' => $this->featureStatus instanceof Model + ? ! $this->isGrantedOn($spec['permission'], $this->featureStatus) + : false, + 'label' => $spec['label'] + ]; + if ($this->featureStatus[$feature] === 2) { + $this->addElement( + 'select', + $feature, + $options + [ + 'description' => t('Multiple Values'), + 'options' => [ + self::LEAVE_UNCHANGED => t('Leave Unchanged'), + t('Disable All'), + t('Enable All') + ], + 'value' => self::LEAVE_UNCHANGED + ] + ); + $decorator->decorate($this->getElement($feature)); + + $this->getElement($feature) + ->getWrapper() + ->getAttributes() + ->add('class', 'indeterminate'); + } else { + $options['value'] = (bool) $this->featureStatus[$feature]; + $this->addElement('checkbox', $feature, $options); + $decorator->decorate($this->getElement($feature)); + } + } + } + + protected function assembleSubmitButton() + { + } + + protected function getCommands(Traversable $objects): Traversable + { + foreach ($this->features as $feature => $spec) { + if ($this->getElement($feature) instanceof CheckboxElement) { + $state = $this->getElement($feature)->isChecked(); + } else { + $state = $this->getElement($feature)->getValue(); + } + + if ($state === self::LEAVE_UNCHANGED || (int) $state === (int) $this->featureStatus[$feature]) { + continue; + } + + $granted = $this->filterGrantedOn($spec['permission'], $objects); + + if ($granted->valid()) { + $command = new ToggleObjectFeatureCommand(); + $command->setObjects($granted); + $command->setFeature($feature); + $command->setEnabled((int) $state); + + $this->submittedFeatures[] = $command; + + yield $command; + } + } + } +} diff --git a/application/forms/DatabaseConfigForm.php b/application/forms/DatabaseConfigForm.php new file mode 100644 index 0000000..7a6c1bd --- /dev/null +++ b/application/forms/DatabaseConfigForm.php @@ -0,0 +1,33 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms; + +use Icinga\Data\ResourceFactory; +use Icinga\Forms\ConfigForm; + +class DatabaseConfigForm extends ConfigForm +{ + public function init() + { + $this->setSubmitLabel(t('Save Changes')); + } + + public function createElements(array $formData) + { + $dbResources = ResourceFactory::getResourceConfigs('db')->keys(); + + $this->addElement('select', 'icingadb_resource', [ + 'description' => t('Database resource'), + 'label' => t('Database'), + 'multiOptions' => array_merge( + ['' => sprintf(' - %s - ', t('Please choose'))], + array_combine($dbResources, $dbResources) + ), + 'disable' => [''], + 'required' => true, + 'value' => '' + ]); + } +} diff --git a/application/forms/Navigation/ActionForm.php b/application/forms/Navigation/ActionForm.php new file mode 100644 index 0000000..08cba3f --- /dev/null +++ b/application/forms/Navigation/ActionForm.php @@ -0,0 +1,58 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Navigation; + +use Icinga\Exception\ConfigurationError; +use Icinga\Forms\Navigation\NavigationItemForm; +use Icinga\Module\Icingadb\Common\Auth; + +class ActionForm extends NavigationItemForm +{ + use Auth; + + /** + * The name of the restriction to which the filter should be applied + * + * @var string + */ + protected $restriction; + + public function createElements(array $formData) + { + parent::createElements($formData); + + $this->addElement( + 'text', + 'filter', + array( + 'allowEmpty' => true, + 'label' => $this->translate('Filter'), + 'description' => $this->translate( + 'Display this action only for objects matching this filter. Leave it blank' + . ' if you want this action being displayed regardless of the object' + ) + ) + ); + } + + public function isValid($formData): bool + { + if (! parent::isValid($formData)) { + return false; + } + + if (($filterString = $this->getValue('filter')) !== null) { + try { + $this->parseRestriction($filterString, $this->restriction); + } catch (ConfigurationError $err) { + $this->getElement('filter')->addError($err->getMessage()); + + return false; + } + } + + return true; + } +} diff --git a/application/forms/Navigation/IcingadbHostActionForm.php b/application/forms/Navigation/IcingadbHostActionForm.php new file mode 100644 index 0000000..adee11d --- /dev/null +++ b/application/forms/Navigation/IcingadbHostActionForm.php @@ -0,0 +1,10 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Navigation; + +class IcingadbHostActionForm extends ActionForm +{ + protected $restriction = 'icingadb/filter/hosts'; +} diff --git a/application/forms/Navigation/IcingadbServiceActionForm.php b/application/forms/Navigation/IcingadbServiceActionForm.php new file mode 100644 index 0000000..29d33c8 --- /dev/null +++ b/application/forms/Navigation/IcingadbServiceActionForm.php @@ -0,0 +1,10 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms\Navigation; + +class IcingadbServiceActionForm extends ActionForm +{ + protected $restriction = 'icingadb/filter/services'; +} diff --git a/application/forms/RedisConfigForm.php b/application/forms/RedisConfigForm.php new file mode 100644 index 0000000..bd3db9c --- /dev/null +++ b/application/forms/RedisConfigForm.php @@ -0,0 +1,606 @@ +<?php + +/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms; + +use Closure; +use Exception; +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Exception\NotWritableError; +use Icinga\File\Storage\LocalFileStorage; +use Icinga\File\Storage\TemporaryLocalFileStorage; +use Icinga\Forms\ConfigForm; +use Icinga\Module\Icingadb\Common\IcingaRedis; +use Icinga\Web\Form; +use ipl\Validator\PrivateKeyValidator; +use ipl\Validator\X509CertValidator; +use Zend_Validate_Callback; + +class RedisConfigForm extends ConfigForm +{ + public function init() + { + $this->setSubmitLabel(t('Save Changes')); + $this->setValidatePartial(true); + } + + public function createElements(array $formData) + { + $this->addElement('checkbox', 'redis_tls', [ + 'label' => t('Use TLS'), + 'description' => t('Encrypt connections to Redis via TLS'), + 'autosubmit' => true + ]); + + $this->addElement('hidden', 'redis_ca'); + $this->addElement('hidden', 'redis_cert'); + $this->addElement('hidden', 'redis_key'); + $this->addElement('hidden', 'clear_redis_ca', ['ignore' => true]); + $this->addElement('hidden', 'clear_redis_cert', ['ignore' => true]); + $this->addElement('hidden', 'clear_redis_key', ['ignore' => true]); + + $useTls = isset($formData['redis_tls']) && $formData['redis_tls']; + if ($useTls) { + $this->addElement('textarea', 'redis_ca_pem', [ + 'label' => t('Redis CA Certificate'), + 'description' => sprintf( + t('Verify the peer using this PEM-encoded CA certificate ("%s...")'), + '-----BEGIN CERTIFICATE-----' + ), + 'required' => true, + 'ignore' => true, + 'validators' => [$this->wrapIplValidator(X509CertValidator::class, 'redis_ca_pem')] + ]); + + $this->addElement('textarea', 'redis_cert_pem', [ + 'label' => t('Client Certificate'), + 'description' => sprintf( + t('Authenticate using this PEM-encoded client certificate ("%s...")'), + '-----BEGIN CERTIFICATE-----' + ), + 'ignore' => true, + 'allowEmpty' => false, + 'validators' => [ + $this->wrapIplValidator(X509CertValidator::class, 'redis_cert_pem', function ($value) { + if (! $value && $this->getElement('redis_key_pem')->getValue()) { + $this->getElement('redis_cert_pem')->addError(t( + 'Either both a client certificate and its private key or none of them must be specified' + )); + } + + return true; + }) + ] + ]); + + $this->addElement('textarea', 'redis_key_pem', [ + 'label' => t('Client Key'), + 'description' => sprintf( + t('Authenticate using this PEM-encoded private key ("%s...")'), + '-----BEGIN PRIVATE KEY-----' + ), + 'ignore' => true, + 'allowEmpty' => false, + 'validators' => [ + $this->wrapIplValidator(PrivateKeyValidator::class, 'redis_key_pem', function ($value) { + if (! $value && $this->getElement('redis_cert_pem')->getValue()) { + $this->getElement('redis_key_pem')->addError(t( + 'Either both a client certificate and its private key or none of them must be specified' + )); + } + + return true; + }) + ] + ]); + } + + $this->addDisplayGroup( + ['redis_tls', 'redis_insecure', 'redis_ca_pem', 'redis_cert_pem', 'redis_key_pem'], + 'redis', + [ + 'decorators' => [ + 'FormElements', + ['HtmlTag', ['tag' => 'div']], + [ + 'Description', + ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend'] + ], + 'Fieldset' + ], + 'description' => t( + 'Secure connections. If you are running a high availability zone' + . ' with two masters, the following applies to both of them.' + ), + 'legend' => t('General') + ] + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + // In case another error occured and the checkbox was displayed before + static::addSkipValidationCheckbox($this); + } + + if ($useTls && isset($formData['redis_insecure']) && $formData['redis_insecure']) { + // In case another error occured and the checkbox was displayed before + static::addInsecureCheckboxIfTls($this); + } + + $this->addElement('text', 'redis1_host', [ + 'description' => t('Redis Host'), + 'label' => t('Redis Host'), + 'required' => true + ]); + + $this->addElement('number', 'redis1_port', [ + 'description' => t('Redis Port'), + 'label' => t('Redis Port'), + 'placeholder' => 6380 + ]); + + $this->addElement('password', 'redis1_password', [ + 'description' => t('Redis Password'), + 'label' => t('Redis Password'), + 'renderPassword' => true, + 'autocomplete' => 'new-password' + ]); + + $this->addDisplayGroup( + ['redis1_host', 'redis1_port', 'redis1_password'], + 'redis1', + [ + 'decorators' => [ + 'FormElements', + ['HtmlTag', ['tag' => 'div']], + [ + 'Description', + ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend'] + ], + 'Fieldset' + ], + 'description' => t( + 'Redis connection details of your Icinga host. If you are running a high' + . ' availability zone with two masters, this is your configuration master.' + ), + 'legend' => t('Primary Icinga Master') + ] + ); + + $this->addElement('text', 'redis2_host', [ + 'description' => t('Redis Host'), + 'label' => t('Redis Host'), + ]); + + $this->addElement('number', 'redis2_port', [ + 'description' => t('Redis Port'), + 'label' => t('Redis Port'), + 'placeholder' => 6380 + ]); + + $this->addElement('password', 'redis2_password', [ + 'description' => t('Redis Password'), + 'label' => t('Redis Password'), + 'renderPassword' => true, + 'autocomplete' => 'new-password' + ]); + + $this->addDisplayGroup( + ['redis2_host', 'redis2_port', 'redis2_password'], + 'redis2', + [ + 'decorators' => [ + 'FormElements', + ['HtmlTag', ['tag' => 'div']], + [ + 'Description', + ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend'] + ], + 'Fieldset' + ], + 'description' => t( + 'If you are running a high availability zone with two masters,' + . ' please provide the Redis connection details of the secondary master.' + ), + 'legend' => t('Secondary Icinga Master') + ] + ); + } + + public static function addSkipValidationCheckbox(Form $form) + { + $form->addElement( + 'checkbox', + 'skip_validation', + [ + 'order' => 0, + 'ignore' => true, + 'label' => t('Skip Validation'), + 'description' => t( + 'Check this box to enforce changes without validating that Redis is available.' + ) + ] + ); + } + + public static function addInsecureCheckboxIfTls(Form $form) + { + if ($form->getElement('redis_insecure') !== null) { + return; + } + + $form->addElement( + 'checkbox', + 'redis_insecure', + [ + 'order' => 1, + 'label' => t('Insecure'), + 'description' => t('Don\'t verify the peer') + ] + ); + + $displayGroup = $form->getDisplayGroup('redis'); + $elements = $displayGroup->getElements(); + $elements['redis_insecure'] = $form->getElement('redis_insecure'); + $displayGroup->setElements($elements); + } + + public function isValid($formData) + { + if (! parent::isValid($formData)) { + return false; + } + + if (($el = $this->getElement('skip_validation')) === null || ! $el->isChecked()) { + if (! static::checkRedis($this)) { + if ($el === null) { + static::addSkipValidationCheckbox($this); + + if ($this->getElement('redis_tls')->isChecked()) { + static::addInsecureCheckboxIfTls($this); + } + } + + return false; + } + } + + return true; + } + + public function isValidPartial(array $formData) + { + if (! parent::isValidPartial($formData)) { + return false; + } + + $useTls = $this->getElement('redis_tls')->isChecked(); + foreach (['ca', 'cert', 'key'] as $name) { + $textareaName = 'redis_' . $name . '_pem'; + $clearName = 'clear_redis_' . $name; + + if ($useTls) { + $this->getElement($clearName)->setValue(null); + + $pemPath = $this->getValue('redis_' . $name); + if ($pemPath && ! isset($formData[$textareaName]) && ! $formData[$clearName]) { + $this->getElement($textareaName)->setValue(@file_get_contents($pemPath)); + } + } + + if (isset($formData[$textareaName]) && ! $formData[$textareaName]) { + $this->getElement($clearName)->setValue(true); + } + } + + if ($this->getElement('backend_validation')->isChecked()) { + if (! static::checkRedis($this)) { + if ($this->getElement('redis_tls')->isChecked()) { + static::addInsecureCheckboxIfTls($this); + } + + return false; + } + } + + return true; + } + + public function onRequest() + { + $errors = []; + + $redisConfig = $this->config->getSection('redis'); + if ($redisConfig->get('tls', false)) { + foreach (['ca', 'cert', 'key'] as $name) { + $path = $redisConfig->get($name); + if (file_exists($path)) { + try { + $redisConfig[$name . '_pem'] = file_get_contents($path); + } catch (Exception $e) { + $errors['redis_' . $name . '_pem'] = sprintf( + t('Failed to read file "%s": %s'), + $path, + $e->getMessage() + ); + } + } + } + } + + $connectionConfig = Config::fromIni( + join(DIRECTORY_SEPARATOR, [dirname($this->config->getConfigFile()), 'redis.ini']) + ); + $this->config->setSection('redis1', [ + 'host' => $connectionConfig->get('redis1', 'host'), + 'port' => $connectionConfig->get('redis1', 'port'), + 'password' => $connectionConfig->get('redis1', 'password') + ]); + $this->config->setSection('redis2', [ + 'host' => $connectionConfig->get('redis2', 'host'), + 'port' => $connectionConfig->get('redis2', 'port'), + 'password' => $connectionConfig->get('redis2', 'password') + ]); + + parent::onRequest(); + + foreach ($errors as $elementName => $message) { + $this->getElement($elementName)->addError($message); + } + } + + public function onSuccess() + { + $storage = new LocalFileStorage(Icinga::app()->getStorageDir( + join(DIRECTORY_SEPARATOR, ['modules', 'icingadb', 'redis']) + )); + + $useTls = $this->getElement('redis_tls')->isChecked(); + $pem = null; + foreach (['ca', 'cert', 'key'] as $name) { + $textarea = $this->getElement('redis_' . $name . '_pem'); + if ($useTls && $textarea !== null && ($pem = $textarea->getValue())) { + $pemFile = md5($pem) . '-' . $name . '.pem'; + if (! $storage->has($pemFile)) { + try { + $storage->create($pemFile, $pem); + } catch (NotWritableError $e) { + $textarea->addError($e->getMessage()); + return false; + } + } + + $this->getElement('redis_' . $name)->setValue($storage->resolvePath($pemFile)); + } + + if ((! $useTls && $this->getElement('clear_redis_' . $name)->getValue()) || ($useTls && ! $pem)) { + $pemPath = $this->getValue('redis_' . $name); + if ($pemPath && $storage->has(($pemFile = basename($pemPath)))) { + try { + $storage->delete($pemFile); + $this->getElement('redis_' . $name)->setValue(null); + } catch (NotWritableError $e) { + $this->addError($e->getMessage()); + return false; + } + } + } + } + + $connectionConfig = Config::fromIni( + join(DIRECTORY_SEPARATOR, [dirname($this->config->getConfigFile()), 'redis.ini']) + ); + + $redis1Host = $this->getValue('redis1_host'); + $redis1Port = $this->getValue('redis1_port'); + $redis1Password = $this->getValue('redis1_password'); + $redis1Section = $connectionConfig->getSection('redis1'); + $redis1Section['host'] = $redis1Host; + $this->getElement('redis1_host')->setValue(null); + $connectionConfig->setSection('redis1', $redis1Section); + if (! empty($redis1Port)) { + $redis1Section['port'] = $redis1Port; + $this->getElement('redis1_port')->setValue(null); + } else { + $redis1Section['port'] = null; + } + + if (! empty($redis1Password)) { + $redis1Section['password'] = $redis1Password; + $this->getElement('redis1_password')->setValue(null); + } else { + $redis1Section['password'] = null; + } + + if (! array_filter($redis1Section->toArray())) { + $connectionConfig->removeSection('redis1'); + } + + $redis2Host = $this->getValue('redis2_host'); + $redis2Port = $this->getValue('redis2_port'); + $redis2Password = $this->getValue('redis2_password'); + $redis2Section = $connectionConfig->getSection('redis2'); + if (! empty($redis2Host)) { + $redis2Section['host'] = $redis2Host; + $this->getElement('redis2_host')->setValue(null); + $connectionConfig->setSection('redis2', $redis2Section); + } else { + $redis2Section['host'] = null; + } + + if (! empty($redis2Port)) { + $redis2Section['port'] = $redis2Port; + $this->getElement('redis2_port')->setValue(null); + $connectionConfig->setSection('redis2', $redis2Section); + } else { + $redis2Section['port'] = null; + } + + if (! empty($redis2Password)) { + $redis2Section['password'] = $redis2Password; + $this->getElement('redis2_password')->setValue(null); + } else { + $redis2Section['password'] = null; + } + + if (! array_filter($redis2Section->toArray())) { + $connectionConfig->removeSection('redis2'); + } + + $connectionConfig->saveIni(); + + return parent::onSuccess(); + } + + public function addSubmitButton() + { + parent::addSubmitButton() + ->getElement('btn_submit') + ->setDecorators(['ViewHelper']); + + $this->addElement( + 'submit', + 'backend_validation', + [ + 'ignore' => true, + 'label' => t('Validate Configuration'), + 'data-progress-label' => t('Validation In Progress'), + 'decorators' => ['ViewHelper'] + ] + ); + $this->addDisplayGroup( + ['btn_submit', 'backend_validation'], + 'submit_validation', + [ + 'decorators' => [ + 'FormElements', + ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']] + ] + ] + ); + + return $this; + } + + public static function checkRedis(Form $form): bool + { + $sections = []; + + $storage = new TemporaryLocalFileStorage(); + foreach (ConfigForm::transformEmptyValuesToNull($form->getValues()) as $sectionAndPropertyName => $value) { + if ($value !== null) { + list($section, $property) = explode('_', $sectionAndPropertyName, 2); + if (in_array($property, ['ca', 'cert', 'key'])) { + $storage->create("$property.pem", $value); + $value = $storage->resolvePath("$property.pem"); + } + + $sections[$section][$property] = $value; + } + } + + $ignoredTextAreas = [ + 'ca' => 'redis_ca_pem', + 'cert' => 'redis_cert_pem', + 'key' => 'redis_key_pem' + ]; + foreach ($ignoredTextAreas as $name => $textareaName) { + if (($textarea = $form->getElement($textareaName)) !== null) { + if (($pem = $textarea->getValue())) { + if ($storage->has("$name.pem")) { + $storage->update("$name.pem", $pem); + } else { + $storage->create("$name.pem", $pem); + $sections['redis'][$name] = $storage->resolvePath("$name.pem"); + } + } elseif ($storage->has("$name.pem")) { + $storage->delete("$name.pem"); + unset($sections['redis'][$name]); + } + } + } + + $moduleConfig = new Config(); + $moduleConfig->setSection('redis', $sections['redis']); + $redisConfig = new Config(); + $redisConfig->setSection('redis1', $sections['redis1'] ?? []); + $redisConfig->setSection('redis2', $sections['redis2'] ?? []); + + try { + $redis1 = IcingaRedis::getPrimaryRedis($moduleConfig, $redisConfig); + } catch (Exception $e) { + $form->warning(sprintf( + t('Failed to connect to primary Redis: %s'), + $e->getMessage() + )); + return false; + } + + if (IcingaRedis::getLastIcingaHeartbeat($redis1) === null) { + $form->warning(t('Primary connection established but failed to verify Icinga is connected as well.')); + return false; + } + + try { + $redis2 = IcingaRedis::getSecondaryRedis($moduleConfig, $redisConfig); + } catch (Exception $e) { + $form->warning(sprintf(t('Failed to connect to secondary Redis: %s'), $e->getMessage())); + return false; + } + + if ($redis2 !== null && IcingaRedis::getLastIcingaHeartbeat($redis2) === null) { + $form->warning(t('Secondary connection established but failed to verify Icinga is connected as well.')); + return false; + } + + $form->info(t('The configuration has been successfully validated.')); + return true; + } + + /** + * Wraps the given IPL validator class into a callback validator + * for usage as the only validator of the element given by name. + * + * @param string $cls IPL validator class FQN + * @param string $element Form element name + * @param Closure $additionalValidator + * + * @return array Callback validator + */ + private function wrapIplValidator(string $cls, string $element, Closure $additionalValidator = null): array + { + return [ + 'Callback', + false, + [ + 'callback' => function ($v) use ($cls, $element, $additionalValidator) { + if ($additionalValidator !== null) { + if (! $additionalValidator($v)) { + return false; + } + } + + if (! $v) { + return true; + } + + $validator = new $cls(); + $valid = $validator->isValid($v); + + if (! $valid) { + /** @var Zend_Validate_Callback $callbackValidator */ + $callbackValidator = $this->getElement($element)->getValidator('Callback'); + + $callbackValidator->setMessage( + $validator->getMessages()[0], + Zend_Validate_Callback::INVALID_VALUE + ); + } + + return $valid; + } + ] + ]; + } +} diff --git a/application/forms/SetAsBackendForm.php b/application/forms/SetAsBackendForm.php new file mode 100644 index 0000000..2840633 --- /dev/null +++ b/application/forms/SetAsBackendForm.php @@ -0,0 +1,34 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Forms; + +use Icinga\Module\Icingadb\Hook\IcingadbSupportHook; +use Icinga\Web\Session; +use ipl\Web\Compat\CompatForm; + +class SetAsBackendForm extends CompatForm +{ + protected $defaultAttributes = [ + 'id' => 'setAsBackendForm', + 'class' => 'icinga-controls' + ]; + + protected function assemble() + { + $this->addElement('checkbox', 'backend', [ + 'class' => 'autosubmit', + 'label' => t('Use Icinga DB As Backend'), + 'value' => IcingadbSupportHook::isIcingaDbSetAsPreferredBackend() + ]); + } + + public function onSuccess() + { + Session::getSession()->getNamespace('icingadb')->set( + IcingadbSupportHook::PREFERENCE_NAME, + $this->getElement('backend')->isChecked() + ); + } +} |