diff --git a/application/controllers/CommandTransportController.php b/application/controllers/CommandTransportController.php new file mode 100644 index 00000000..15264eea --- /dev/null +++ b/application/controllers/CommandTransportController.php @@ -0,0 +1,147 @@ +assertPermission('config/modules'); + } + + public function indexAction() + { + $list = new CommandTransportList((new CommandTransportConfig())->select()); + + $this->addControl( + (new ButtonLink( + t('Create Command Transport'), + 'icingadb/command-transport/add', + 'plus' + ))->setBaseTarget('_next') + ); + + $this->addContent($list); + + $this->mergeTabs($this->Module()->getConfigTabs()); + $this->getTabs()->disableLegacyExtensions(); + $this->view->title = $this->getTabs() + ->activate('command-transports') + ->getActiveTab() + ->getLabel(); + } + + public function showAction() + { + $transportName = $this->params->getRequired('name'); + + $transportConfig = (new CommandTransportConfig()) + ->select() + ->where('name', $transportName) + ->fetchRow(); + if ($transportConfig === false) { + $this->httpNotFound(t('Unknown transport')); + } + + $form = new ApiTransportForm(); + $form->populate((array) $transportConfig); + $form->on(ApiTransportForm::ON_SUCCESS, function (ApiTransportForm $form) use ($transportName) { + (new CommandTransportConfig())->update( + 'transport', + $form->getValues(), + Filter::where('name', $transportName) + ); + + $this->redirectNow('icingadb/command-transport'); + }); + + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($form); + + $this->setTitle(sprintf($this->translate('Command Transport: %s'), $transportName)); + $this->getTabs()->disableLegacyExtensions(); + } + + public function addAction() + { + $form = new ApiTransportForm(); + $form->on(ApiTransportForm::ON_SUCCESS, function (ApiTransportForm $form) { + (new CommandTransportConfig())->insert('transport', $form->getValues()); + $this->redirectNow('icingadb/command-transport'); + }); + + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($form); + + $this->setTitle($this->translate('Add Command Transport')); + $this->getTabs()->disableLegacyExtensions(); + } + + public function removeAction() + { + $transportName = $this->params->getRequired('name'); + + $form = new ConfirmRemovalForm(); + $form->setAttrib('style', 'text-align:center;'); + $form->setOnSuccess(function () use ($transportName) { + (new CommandTransportConfig())->delete( + 'transport', + Filter::where('name', $transportName) + ); + + $this->redirectNow('icingadb/command-transport'); + }); + + $form->handleRequest(); + + $this->addContent(HtmlString::create($form->render())); + + $this->setTitle($this->translate('Remove Command Transport: %s'), $transportName); + $this->getTabs()->disableLegacyExtensions(); + } + + public function sortAction() + { + $transportName = $this->params->getRequired('name'); + $newPosition = (int) $this->params->getRequired('pos'); + + $config = $this->Config('commandtransports'); + if (! $config->hasSection($transportName)) { + $this->httpNotFound(t('Unknown transport')); + } + + if ($newPosition < 0 || $newPosition > $config->count()) { + $this->httpBadRequest(t('Position out of bounds')); + } + + $transports = $config->getConfigObject()->toArray(); + $transportNames = array_keys($transports); + + array_splice($transportNames, array_search($transportName, $transportNames, true), 1); + array_splice($transportNames, $newPosition, 0, [$transportName]); + + $sortedTransports = []; + foreach ($transportNames as $name) { + $sortedTransports[$name] = $transports[$name]; + } + + $newConfig = Config::fromArray($sortedTransports); + $newConfig->saveIni($config->getConfigFile()); + + $this->redirectNow('icingadb/command-transport'); + } +} diff --git a/application/controllers/CommentsController.php b/application/controllers/CommentsController.php index 466f2d99..d0b75ca4 100644 --- a/application/controllers/CommentsController.php +++ b/application/controllers/CommentsController.php @@ -6,19 +6,16 @@ namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ContinueWith; use Icinga\Module\Icingadb\Widget\ItemList\CommentList; use Icinga\Module\Icingadb\Widget\ShowMore; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentsCommandForm; -use ipl\Html\HtmlDocument; -use ipl\Html\HtmlString; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\ActionLink; -use ipl\Web\Widget\Icon; class CommentsController extends Controller { @@ -109,6 +106,8 @@ class CommentsController extends Controller public function deleteAction() { + // TODO: Check permission + $this->setTitle(t('Remove Comments')); $db = $this->getDb(); @@ -124,40 +123,19 @@ class CommentsController extends Controller $this->filter($comments); - $deleteCommentsForm = (new DeleteCommentsCommandForm()) - ->addDescription(sprintf( - t('Confirm removal of %d comments.'), - $comments->count() - )) - ->setComments($comments) - ->setRedirectUrl(Links::comments()) - ->create(); + $form = (new DeleteCommentForm()) + ->setObjects($comments) + ->setRedirectUrl(Links::comments()->getAbsoluteUrl()) + ->on(DeleteCommentForm::ON_SUCCESS, function ($form) { + // This forces the column to reload nearly instantly after the redirect + // and ensures the effect of the command is visible to the user asap + $this->getResponse()->setAutoRefreshInterval(1); - $deleteCommentsForm->removeElement('btn_submit'); + $this->redirectNow($form->getRedirectUrl()); + }) + ->handleRequest(ServerRequest::fromGlobals()); - $deleteCommentsForm->addElement( - 'button', - 'btn_submit', - [ - 'class' => 'cancel-button spinner', - 'decorators' => [ - 'ViewHelper', - ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']] - ], - 'escape' => false, - 'ignore' => true, - 'label' => (new HtmlDocument()) - ->add([new Icon('trash'), t('Remove Comments')]) - ->setSeparator(' ') - ->render(), - 'title' => t('Remove comments'), - 'type' => 'submit' - ] - ); - - $deleteCommentsForm->handleRequest(); - - $this->addContent(HtmlString::create($deleteCommentsForm->render())); + $this->addContent($form); } public function detailsAction() @@ -191,15 +169,16 @@ class CommentsController extends Controller sprintf(t('Show all %d comments'), $comments->count()) )); - $this->addContent((new ActionLink( - sprintf(t('Remove %d comments'), $comments->count()), - Links::commentsDelete()->setQueryString(QueryString::render($this->getFilter())), - 'trash', - [ - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ] - ))->setAttribute('class', 'cancel-button')); + // TODO: Check permission + $this->addContent( + (new DeleteCommentForm()) + ->setObjects($comments) + ->setAction( + Links::commentsDelete() + ->setQueryString(QueryString::render($this->getFilter())) + ->getAbsoluteUrl() + ) + ); } public function completeAction() diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 74cdf3fa..a56c7c2f 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -4,22 +4,15 @@ namespace Icinga\Module\Icingadb\Controllers; -use Exception; use Icinga\Application\Config; -use Icinga\Exception\NotFoundError; -use Icinga\Forms\ConfirmRemovalForm; use Icinga\Module\Icingadb\Forms\DatabaseConfigForm; use Icinga\Module\Icingadb\Forms\RedisConfigForm; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Monitoring\Forms\Config\SecurityConfigForm; -use Icinga\Module\Monitoring\Forms\Config\TransportConfigForm; -use Icinga\Module\Monitoring\Forms\Config\TransportReorderForm; use Icinga\Web\Form; -use Icinga\Web\Notification; use Icinga\Web\Widget\Tab; use Icinga\Web\Widget\Tabs; use ipl\Html\HtmlString; -use ipl\Web\Widget\ButtonLink; class ConfigController extends Controller { @@ -30,142 +23,6 @@ class ConfigController extends Controller // parent::init(); // } - public function commandTransportsAction() - { - $form = new TransportReorderForm(); - - $form->handleRequest(); - - $this->mergeTabs($this->Module()->getConfigTabs()->activate('command-transports')); - - $this->addControl( - (new ButtonLink( - t('Create Command Transport'), - 'icingadb/config/create-command-transport', - 'plus' - ))->setBaseTarget('_next') - ); - - $this->addFormToContent($form); - } - - public function createCommandTransportAction() - { - $this->setTitle(t('Create Command Transport')); - - $form = (new TransportConfigForm()) - ->setIniConfig(Config::module('monitoring', 'commandtransports')) - ->setRedirectUrl('icingadb/config/command-transports'); - - $form->setOnSuccess(function (TransportConfigForm $form) { - try { - $form->add($form::transformEmptyValuesToNull($form->getValues())); - } catch (Exception $e) { - $form->error($e->getMessage()); - - return false; - } - - if ($form->save()) { - Notification::success(t('Command transport successfully created')); - - return true; - } - - return false; - }); - - $form->handleRequest(); - - $this->addFormToContent($form); - } - - public function deleteCommandTransportAction() - { - $this->setTitle(t('Delete Command Transport')); - - $transportName = $this->params->getRequired('transport'); - - $transportConfigForm = (new TransportConfigForm()) - ->setIniConfig(Config::module('monitoring', 'commandtransports')); - - $confirmRemovalForm = (new ConfirmRemovalForm()) - ->setRedirectUrl('icingadb/config/command-transports'); - - $confirmRemovalForm->setOnSuccess( - function (ConfirmRemovalForm $form) use ($transportName, $transportConfigForm) { - try { - $transportConfigForm->delete($transportName); - } catch (Exception $e) { - $form->error($e->getMessage()); - - return false; - } - - if ($transportConfigForm->save()) { - Notification::success(sprintf( - t('Command transport "%s" successfully removed'), - $transportName - )); - - return true; - } - - return false; - } - ); - - $confirmRemovalForm->handleRequest(); - - $this->addFormToContent($confirmRemovalForm); - } - - public function updateCommandTransportAction() - { - $this->setTitle(t('Update Command Transport')); - - $transportName = $this->params->getRequired('transport'); - - $form = (new TransportConfigForm()) - ->setIniConfig(Config::module('monitoring', 'commandtransports')) - ->setRedirectUrl('icingadb/config/command-transports'); - - $form->setOnSuccess(function (TransportConfigForm $form) use ($transportName) { - try { - $form->edit($transportName, array_map( - function ($v) { - return $v !== '' ? $v : null; - }, - $form->getValues() - )); - } catch (Exception $e) { - $form->error($e->getMessage()); - - return false; - } - - if ($form->save()) { - Notification::success(sprintf( - t('Command transport "%s" successfully updated'), - $transportName - )); - - return true; - } - - return false; - }); - - try { - $form->load($transportName); - $form->handleRequest(); - } catch (NotFoundError $_) { - $this->httpNotFound(sprintf(t('Command transport "%s" not found'), $transportName)); - } - - $this->addFormToContent($form); - } - public function databaseAction() { $form = (new DatabaseConfigForm()) diff --git a/application/controllers/DowntimesController.php b/application/controllers/DowntimesController.php index c5ddc870..44a9ce04 100644 --- a/application/controllers/DowntimesController.php +++ b/application/controllers/DowntimesController.php @@ -6,6 +6,7 @@ namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; @@ -114,6 +115,8 @@ class DowntimesController extends Controller public function deleteAction() { + // TODO: Check permission + $this->setTitle(t('Cancel Downtimes')); $db = $this->getDb(); @@ -129,40 +132,19 @@ class DowntimesController extends Controller $this->filter($downtimes); - $cancelDowntimesForm = (new DeleteDowntimesCommandForm()) - ->addDescription(sprintf( - t('Confirm cancellation of %d downtimes.'), - $downtimes->count() - )) - ->setDowntimes($downtimes) - ->setRedirectUrl(Links::downtimes()) - ->create(); + $form = (new DeleteDowntimeForm()) + ->setObjects($downtimes) + ->setRedirectUrl(Links::downtimes()->getAbsoluteUrl()) + ->on(DeleteDowntimeForm::ON_SUCCESS, function ($form) { + // This forces the column to reload nearly instantly after the redirect + // and ensures the effect of the command is visible to the user asap + $this->getResponse()->setAutoRefreshInterval(1); - $cancelDowntimesForm->removeElement('btn_submit'); + $this->redirectNow($form->getRedirectUrl()); + }) + ->handleRequest(ServerRequest::fromGlobals()); - $cancelDowntimesForm->addElement( - 'button', - 'btn_submit', - [ - 'class' => 'cancel-button spinner', - 'decorators' => [ - 'ViewHelper', - ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']] - ], - 'escape' => false, - 'ignore' => true, - 'label' => (new HtmlDocument()) - ->add([new Icon('trash'), t('Cancel Downtimes')]) - ->setSeparator(' ') - ->render(), - 'title' => t('Cancel downtimes'), - 'type' => 'submit' - ] - ); - - $cancelDowntimesForm->handleRequest(); - - $this->addContent(HtmlString::create($cancelDowntimesForm->render())); + $this->addContent($form); } public function detailsAction() @@ -196,15 +178,16 @@ class DowntimesController extends Controller sprintf(t('Show all %d downtimes'), $downtimes->count()) )); - $this->addContent((new ActionLink( - sprintf(t('Cancel %d downtimes'), $downtimes->count()), - Links::downtimesDelete()->setQueryString(QueryString::render($this->getFilter())), - 'trash', - [ - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ] - ))->setAttribute('class', 'cancel-button')); + // TODO: Check permission + $this->addContent( + (new DeleteDowntimeForm()) + ->setObjects($downtimes) + ->setAction( + Links::downtimesDelete() + ->setQueryString(QueryString::render($this->getFilter())) + ->getAbsoluteUrl() + ) + ); } public function completeAction() diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php index 373eac37..1004ddb7 100644 --- a/application/controllers/HealthController.php +++ b/application/controllers/HealthController.php @@ -4,16 +4,18 @@ namespace Icinga\Module\Icingadb\Controllers; -use Icinga\Module\Icingadb\Compat\CompatBackend; +use GuzzleHttp\Psr7\ServerRequest; +use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand; +use Icinga\Module\Icingadb\Common\Links; +use Icinga\Module\Icingadb\Forms\Command\Instance\ToggleInstanceFeaturesForm; use Icinga\Module\Icingadb\Model\HoststateSummary; use Icinga\Module\Icingadb\Model\Instance; use Icinga\Module\Icingadb\Model\ServicestateSummary; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Health; use Icinga\Module\Icingadb\Widget\VerticalKeyValue; -use Icinga\Module\Monitoring\Forms\Command\Instance\ToggleInstanceFeaturesCommandForm; use ipl\Html\Html; -use ipl\Html\HtmlString; +use ipl\Web\Url; class HealthController extends Controller { @@ -83,22 +85,27 @@ class HealthController extends Controller ['class' => 'instance-commands'], Html::tag('h2', t('Feature Commands')) ); - $programStatus = (object) [ - 'active_host_checks_enabled' => $instance->icinga2_active_host_checks_enabled, - 'active_service_checks_enabled' => $instance->icinga2_active_service_checks_enabled, - 'event_handlers_enabled' => $instance->icinga2_event_handlers_enabled, - 'flap_detection_enabled' => $instance->icinga2_flap_detection_enabled, - 'notifications_enabled' => $instance->icinga2_notifications_enabled, - 'process_performance_data' => $instance->icinga2_performance_data_enabled, - 'program_version' => $instance->icinga2_version - ]; - $toggleInstanceFeaturesCommandForm = new ToggleInstanceFeaturesCommandForm(); - $toggleInstanceFeaturesCommandForm - ->setBackend(new CompatBackend()) - ->setStatus($programStatus) - ->load($programStatus) - ->handleRequest(); - $featureCommands->add(HtmlString::create($toggleInstanceFeaturesCommandForm->render())); + $toggleInstanceFeaturesCommandForm = new ToggleInstanceFeaturesForm([ + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS => + $instance->icinga2_active_host_checks_enabled, + ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS => + $instance->icinga2_active_service_checks_enabled, + ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS => + $instance->icinga2_event_handlers_enabled, + ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION => + $instance->icinga2_flap_detection_enabled, + ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS => + $instance->icinga2_notifications_enabled, + ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA => + $instance->icinga2_performance_data_enabled + ]); + $toggleInstanceFeaturesCommandForm->setObjects([$instance]); + $toggleInstanceFeaturesCommandForm->on(ToggleInstanceFeaturesForm::ON_SUCCESS, function () { + $this->redirectNow(Url::fromPath('icingadb/health')->getAbsoluteUrl()); + }); + $toggleInstanceFeaturesCommandForm->handleRequest(ServerRequest::fromGlobals()); + + $featureCommands->add($toggleInstanceFeaturesCommandForm); $this->addContent($featureCommands); $this->setAutorefreshInterval(30); diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php index e2f9bf1f..be933a76 100644 --- a/application/controllers/HostsController.php +++ b/application/controllers/HostsController.php @@ -7,9 +7,9 @@ namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; -use Icinga\Module\Icingadb\Compat\FeatureStatus; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\HoststateSummary; +use Icinga\Module\Icingadb\Util\FeatureStatus; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ContinueWith; diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php index cec1ee03..0cfb05f0 100644 --- a/application/controllers/ServicesController.php +++ b/application/controllers/ServicesController.php @@ -7,9 +7,9 @@ namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; -use Icinga\Module\Icingadb\Compat\FeatureStatus; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicestateSummary; +use Icinga\Module\Icingadb\Util\FeatureStatus; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ContinueWith; diff --git a/application/forms/ApiTransportForm.php b/application/forms/ApiTransportForm.php new file mode 100644 index 00000000..c05f8fc2 --- /dev/null +++ b/application/forms/ApiTransportForm.php @@ -0,0 +1,102 @@ +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') + ]); + + // TODO: Use a password element + $this->addElement('text', 'password', [ + 'required' => true, + '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') === 'n') { + 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 00000000..8145bf99 --- /dev/null +++ b/application/forms/Command/CommandForm.php @@ -0,0 +1,125 @@ + 'icinga-form icinga-controls']; + + /** @var mixed */ + protected $objects; + + /** + * Set the objects to issue the command for + * + * @param mixed $objects A traversable that is also countable + * + * @return $this + */ + public function setObjects($objects) + { + $this->objects = $objects; + + return $this; + } + + /** + * Get the objects to issue the command for + * + * @return mixed + */ + public function getObjects() + { + return $this->objects; + } + + /** + * 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 command to issue for the given object + * + * @param Model $object + * + * @return IcingaCommand|IcingaCommand[]|null NULL in case no command should be issued for the object + */ + abstract protected function getCommand(Model $object); + + protected function assemble() + { + $this->assembleElements(); + $this->assembleSubmitButton(); + $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + } + + protected function onSuccess() + { + $errors = []; + foreach ($this->getObjects() as $object) { + $commands = $this->getCommand($object); + if ($commands === null) { + continue; + } + + if ($commands instanceof IcingaCommand) { + $commands = [$commands]; + } + + foreach ($commands 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.') + ); + } + + Notification::error($errors[0]); + } + } + + /** + * Transmit the given command + * + * @param IcingaCommand $command + * + * @return void + */ + protected function sendCommand(IcingaCommand $command) + { + (new CommandTransport())->send($command); + } +} diff --git a/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php b/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php new file mode 100644 index 00000000..b28dbd59 --- /dev/null +++ b/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php @@ -0,0 +1,82 @@ +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'); + } + + protected function assembleElements() + { + $disabled = ! $this->getAuth()->hasPermission('monitoring/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 getCommand(Model $object) + { + 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((int) $featureState); + + yield $command; + } + } +} diff --git a/application/forms/Command/Object/AcknowledgeProblemForm.php b/application/forms/Command/Object/AcknowledgeProblemForm.php new file mode 100644 index 00000000..9e906387 --- /dev/null +++ b/application/forms/Command/Object/AcknowledgeProblemForm.php @@ -0,0 +1,159 @@ +add(new HtmlElement('div', ['class' => 'form-description'], [ + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement('ul', null, [ + new HtmlElement('li', null, 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')); + + $acknowledgeExpire = (bool) $config->get('settings', 'acknowledge_expire', false); + + $this->addElement( + 'checkbox', + 'expire', + [ + 'ignore' => true, + 'class' => 'autosubmit', + 'value' => $acknowledgeExpire, + 'label' => t('Use Expire Time'), + 'description' => t('If the acknowledgement should expire, check this option.') + ] + ); + $decorator->decorate($this->getElement('expire')); + + if ($acknowledgeExpire || $this->getPopulatedValue('expire') === 'y') { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval($config->get('settings', 'acknowledge_expire_time', 'PT1H'))); + + $this->addElement( + 'localDateTime', + 'expire_time', + [ + 'required' => true, + 'value' => $expireTime, + 'label' => t('Expire Time'), + 'description' => t('Choose the date and time when Icinga should delete the acknowledgement.') + ] + ); + $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 getCommand(Model $object) + { + $command = new AcknowledgeProblemCommand(); + $command->setObject($object); + $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()); + } + + return $command; + } +} diff --git a/application/forms/Command/Object/AddCommentForm.php b/application/forms/Command/Object/AddCommentForm.php new file mode 100644 index 00000000..bb2cc05c --- /dev/null +++ b/application/forms/Command/Object/AddCommentForm.php @@ -0,0 +1,110 @@ +add(new HtmlElement('div', ['class' => 'form-description'], [ + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement('ul', null, [ + new HtmlElement('li', null, 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'); + $commentExpire = (bool) $config->get('settings', 'comment_expire', false); + + $this->addElement( + 'checkbox', + 'expire', + [ + 'ignore' => true, + 'class' => 'autosubmit', + 'value' => $commentExpire, + 'label' => t('Use Expire Time'), + 'description' => t('If the comment should expire, check this option.') + ] + ); + $decorator->decorate($this->getElement('expire')); + + if ($commentExpire || $this->getPopulatedValue('expire') === 'y') { + $expireTime = new DateTime(); + $expireTime->add(new DateInterval($config->get('settings', 'comment_expire_time', 'PT1H'))); + + $this->addElement( + 'localDateTime', + 'expire_time', + [ + 'required' => true, + 'value' => $expireTime, + 'label' => t('Expire Time'), + 'description' => t('Choose the date and time when Icinga should delete the comment.') + ] + ); + $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 getCommand(Model $object) + { + $command = new AddCommentCommand(); + $command->setObject($object); + $command->setComment($this->getValue('comment')); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + if (($expireTime = $this->getValue('expire_time'))) { + /** @var DateTime $expireTime */ + $command->setExpireTime($expireTime->getTimestamp()); + } + + return $command; + } +} diff --git a/application/forms/Command/Object/CheckNowForm.php b/application/forms/Command/Object/CheckNowForm.php new file mode 100644 index 00000000..bd7ea476 --- /dev/null +++ b/application/forms/Command/Object/CheckNowForm.php @@ -0,0 +1,63 @@ + 'inline']; + + public function __construct() + { + $this->on(self::ON_SUCCESS, function () { + 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 getCommand(Model $object) + { + if ( + ! $object->active_checks_enabled + && ! $this->getAuth()->hasPermission('monitoring/command/schedule-check') + ) { + return null; + } + + $command = new ScheduleCheckCommand(); + $command->setObject($object); + $command->setCheckTime(time()); + $command->setForced(); + + return $command; + } +} diff --git a/application/forms/Command/Object/DeleteCommentForm.php b/application/forms/Command/Object/DeleteCommentForm.php new file mode 100644 index 00000000..036ce413 --- /dev/null +++ b/application/forms/Command/Object/DeleteCommentForm.php @@ -0,0 +1,47 @@ +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 getCommand(Model $object) + { + $command = new DeleteCommentCommand(); + $command->setCommentName($object->name); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + return $command; + } +} diff --git a/application/forms/Command/Object/DeleteDowntimeForm.php b/application/forms/Command/Object/DeleteDowntimeForm.php new file mode 100644 index 00000000..f1a4bc3e --- /dev/null +++ b/application/forms/Command/Object/DeleteDowntimeForm.php @@ -0,0 +1,49 @@ + 'inline']; + + protected function assembleElements() + { + $this->addElement($this->createRedirectOption()); + } + + protected function assembleSubmitButton() + { + $this->addElement( + 'submitButton', + 'btn_submit', + [ + 'class' => ['cancel-button', 'spinner'], + 'label' => [ + new Icon('trash'), + tp('Delete downtime', 'Delete donwtimes', count($this->getObjects())) + ] + ] + ); + } + + protected function getCommand(Model $object) + { + $command = new DeleteDowntimeCommand(); + $command->setDowntimeName($object->name); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + return $command; + } +} diff --git a/application/forms/Command/Object/ProcessCheckResultForm.php b/application/forms/Command/Object/ProcessCheckResultForm.php new file mode 100644 index 00000000..7e015e01 --- /dev/null +++ b/application/forms/Command/Object/ProcessCheckResultForm.php @@ -0,0 +1,111 @@ +add(new HtmlElement('div', ['class' => 'form-description'], [ + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement('ul', null, [ + new HtmlElement('li', null, t( + 'This command is used to submit passive host or service check results.' + )) + ]) + ])); + + $decorator = new IcingaFormDecorator(); + + foreach ($this->getObjects() as $object) { + /** @var Model $object */ + // Nasty, but as getObjects() returns everything but an object with a real + // iterator interface this is the only way to fetch just the first element + break; + } + + $this->addElement( + 'select', + 'status', + [ + 'required' => true, + 'label' => t('Status'), + 'description' => t('The state this check result should report'), + 'multiOptions' => $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 getCommand(Model $object) + { + $command = new ProcessCheckResultCommand(); + $command->setObject($object); + $command->setStatus($this->getValue('status')); + $command->setOutput($this->getValue('output')); + $command->setPerformanceData($this->getValue('perfdata')); + + return $command; + } +} diff --git a/application/forms/Command/Object/RemoveAcknowledgementForm.php b/application/forms/Command/Object/RemoveAcknowledgementForm.php new file mode 100644 index 00000000..f7e99f3b --- /dev/null +++ b/application/forms/Command/Object/RemoveAcknowledgementForm.php @@ -0,0 +1,46 @@ + '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 getCommand(Model $object) + { + $command = new RemoveAcknowledgementCommand(); + $command->setObject($object); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + return $command; + } +} diff --git a/application/forms/Command/Object/ScheduleCheckForm.php b/application/forms/Command/Object/ScheduleCheckForm.php new file mode 100644 index 00000000..694105cd --- /dev/null +++ b/application/forms/Command/Object/ScheduleCheckForm.php @@ -0,0 +1,81 @@ +add(new HtmlElement('div', ['class' => 'form-description'], [ + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement('ul', null, [ + new HtmlElement('li', null, 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', + [ + '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 getCommand(Model $object) + { + $command = new ScheduleCheckCommand(); + $command->setObject($object); + $command->setForced($this->getElement('force_check')->isChecked()); + $command->setCheckTime($this->getValue('check_time')->getTimestamp()); + + return $command; + } +} diff --git a/application/forms/Command/Object/ScheduleHostDowntimeForm.php b/application/forms/Command/Object/ScheduleHostDowntimeForm.php new file mode 100644 index 00000000..b1a8ef39 --- /dev/null +++ b/application/forms/Command/Object/ScheduleHostDowntimeForm.php @@ -0,0 +1,77 @@ +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' => (bool) $config->get('settings', 'hostdowntime_all_services', false) + ] + ); + $decorator->decorate($this->getElement('all_services')); + + $this->addElement( + 'select', + 'child_options', + array( + 'description' => t('Schedule child downtimes.'), + 'label' => t('Child Options'), + 'multiOptions' => [ + 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 getCommand(Model $object) + { + if (($childOptions = (int) $this->getValue('child_options'))) { + $command = new PropagateHostDowntimeCommand(); + $command->setTriggered($childOptions === 1); + } else { + $command = new ScheduleHostDowntimeCommand(); + } + + $command->setObject($object); + $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 + ); + } + + return $command; + } +} diff --git a/application/forms/Command/Object/ScheduleServiceDowntimeForm.php b/application/forms/Command/Object/ScheduleServiceDowntimeForm.php new file mode 100644 index 00000000..f48ecda1 --- /dev/null +++ b/application/forms/Command/Object/ScheduleServiceDowntimeForm.php @@ -0,0 +1,178 @@ +add(new HtmlElement('div', ['class' => 'form-description'], [ + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement('ul', null, [ + new HtmlElement('li', null, 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.' + ) + ] + ); + $decorator->decorate($this->getElement('comment')); + + $this->addElement( + 'localDateTime', + 'start', + [ + 'required' => true, + 'value' => new DateTime(), + 'label' => t('Start Time'), + 'description' => t('Set the start date and time for the downtime.') + ] + ); + $decorator->decorate($this->getElement('start')); + + $this->addElement( + 'localDateTime', + 'end', + [ + 'required' => true, + 'label' => t('End Time'), + 'description' => t('Set the end date and time for the downtime.'), + 'value' => (new DateTime())->add(new DateInterval('PT1H')), + 'validators' => ['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 ($this->getPopulatedValue('flexible') === 'y') { + $hoursInput = $this->createElement( + 'number', + 'hours', + [ + 'required' => true, + 'label' => t('Duration'), + 'value' => 2, + 'min' => 0 + ] + ); + $this->registerElement($hoursInput); + $decorator->decorate($hoursInput); + + $minutesInput = $this->createElement( + 'number', + 'minutes', + [ + 'required' => true, + 'value' => 0, + 'min' => 0 + ] + ); + $this->registerElement($minutesInput); + $minutesInput->addWrapper( + new HtmlElement('label', null, new HtmlElement('span', null, t('Minutes'))) + ); + + $hoursInput->getWrapper() + ->add($minutesInput) + ->getAttributes()->add('class', 'downtime-duration'); + $hoursInput->prependWrapper( + new HtmlElement('label', null, new HtmlElement('span', null, 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 getCommand(Model $object) + { + $command = new ScheduleServiceDowntimeCommand(); + $command->setObject($object); + $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 + ); + } + + return $command; + } +} diff --git a/application/forms/Command/Object/SendCustomNotificationForm.php b/application/forms/Command/Object/SendCustomNotificationForm.php new file mode 100644 index 00000000..b489eb9e --- /dev/null +++ b/application/forms/Command/Object/SendCustomNotificationForm.php @@ -0,0 +1,86 @@ +add(new HtmlElement('div', ['class' => 'form-description'], [ + new Icon('info-circle', ['class' => 'form-description-icon']), + new HtmlElement('ul', null, [ + new HtmlElement('li', null, 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 getCommand(Model $object) + { + $command = new SendCustomNotificationCommand(); + $command->setObject($object); + $command->setComment($this->getValue('comment')); + $command->setForced($this->getElement('forced')->isChecked()); + $command->setAuthor($this->getAuth()->getUser()->getUsername()); + + return $command; + } +} diff --git a/application/forms/Command/Object/ToggleObjectFeaturesForm.php b/application/forms/Command/Object/ToggleObjectFeaturesForm.php new file mode 100644 index 00000000..bcc93a62 --- /dev/null +++ b/application/forms/Command/Object/ToggleObjectFeaturesForm.php @@ -0,0 +1,113 @@ +featureStatus = $featureStatus; + $this->features = [ + ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => [ + 'label' => t('Active Checks'), + 'permission' => 'monitoring/command/feature/object/active-checks' + ], + ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => [ + 'label' => t('Passive Checks'), + 'permission' => 'monitoring/command/feature/object/passive-checks' + ], + ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => [ + 'label' => t('Notifications'), + 'permission' => 'monitoring/command/feature/object/notifications' + ], + ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => [ + 'label' => t('Event Handler'), + 'permission' => 'monitoring/command/feature/object/event-handler' + ], + ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => [ + 'label' => t('Flap Detection'), + 'permission' => 'monitoring/command/feature/object/flap-detection' + ] + ]; + + $this->getAttributes()->add('class', 'object-features'); + } + + protected function assembleElements() + { + $decorator = new IcingaFormDecorator(); + foreach ($this->features as $feature => $spec) { + $options = [ + 'class' => 'autosubmit', + 'disabled' => ! $this->getAuth()->hasPermission($spec['permission']), + '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') + ] + ] + ); + $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 getCommand(Model $object) + { + foreach ($this->features as $feature => $spec) { + $featureState = $this->getElement($feature)->isChecked(); + + if ( + ! $this->getAuth()->hasPermission($spec['permission']) + || $featureState === self::LEAVE_UNCHANGED + || (int) $featureState === (int) $this->featureStatus[$feature] + ) { + continue; + } + + $command = new ToggleObjectFeatureCommand(); + $command->setObject($object); + $command->setFeature($feature); + $command->setEnabled((int) $featureState); + + yield $command; + } + } +} diff --git a/application/views/scripts/form/reorder-command-transports.phtml b/application/views/scripts/form/reorder-command-transports.phtml deleted file mode 100644 index 03def6ff..00000000 --- a/application/views/scripts/form/reorder-command-transports.phtml +++ /dev/null @@ -1,79 +0,0 @@ - -
- - - - - - - - - - getConfig(); - $total = $transportConfig->count(); - foreach ($transportConfig as $transportName => $config): - ++$i; - ?> - - - - - - - -
translate('Transport') ?>
- qlink( - $transportName, - 'icingadb/config/update-command-transport', - ['transport' => $transportName], - [ - 'icon' => 'edit', - 'title' => sprintf($this->translate('Update command transport %s'), $transportName) - ] - ); ?> - (translate('Type: %s'), - ucfirst($config->get('transport', 'local')) - ) ?>) - - - qlink( - '', - 'icingadb/config/delete-command-transport', - ['transport' => $transportName], - array( - 'class' => 'action-link', - 'icon' => 'cancel', - 'title' => sprintf($this->translate('Delete command transport %s'), $transportName) - ) - ); ?> - - 0): ?> - - - - - -
- getElement($form->getTokenElementName()) ?> - getElement($form->getUidElementName()) ?> -
diff --git a/configuration.php b/configuration.php index 81a8ee4c..600d790a 100644 --- a/configuration.php +++ b/configuration.php @@ -212,7 +212,7 @@ namespace Icinga\Module\Icingadb $this->provideConfigTab('command-transports', [ 'label' => t('Command Transports'), 'title' => t('Configure command transports'), - 'url' => 'config/command-transports' + 'url' => 'command-transport' ]); $this->provideConfigTab('security', [ 'label' => t('Security'), diff --git a/library/Icingadb/Command/IcingaApiCommand.php b/library/Icingadb/Command/IcingaApiCommand.php new file mode 100644 index 00000000..77fb8319 --- /dev/null +++ b/library/Icingadb/Command/IcingaApiCommand.php @@ -0,0 +1,85 @@ +setEndpoint($endpoint) + ->setData($data); + } + + /** + * Get the command data + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Set the command data + * + * @param array $data + * + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + return $this; + } + + /** + * Get the name of the endpoint + * + * @return string + */ + public function getEndpoint() + { + return $this->endpoint; + } + + /** + * Set the name of the endpoint + * + * @param string $endpoint + * + * @return $this + */ + public function setEndpoint($endpoint) + { + $this->endpoint = $endpoint; + + return $this; + } +} diff --git a/library/Icingadb/Command/IcingaCommand.php b/library/Icingadb/Command/IcingaCommand.php new file mode 100644 index 00000000..08a48729 --- /dev/null +++ b/library/Icingadb/Command/IcingaCommand.php @@ -0,0 +1,22 @@ +feature = (string) $feature; + + return $this; + } + + /** + * Get the feature that is to be enabled or disabled + * + * @return string + */ + public function getFeature() + { + return $this->feature; + } + + /** + * Set whether the feature should be enabled or disabled + * + * @param bool $enabled + * + * @return $this + */ + public function setEnabled($enabled = true) + { + $this->enabled = (bool) $enabled; + + return $this; + } + + /** + * Get whether the feature should be enabled or disabled + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } +} diff --git a/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php b/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php new file mode 100644 index 00000000..aaff0394 --- /dev/null +++ b/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php @@ -0,0 +1,140 @@ +sticky = (bool) $sticky; + + return $this; + } + + /** + * Is the acknowledgement sticky? + * + * @return bool + */ + public function getSticky() + { + return $this->sticky; + } + + /** + * Set whether to send a notification about the acknowledgement + * + * @param bool $notify + * + * @return $this + */ + public function setNotify($notify = true) + { + $this->notify = (bool) $notify; + + return $this; + } + + /** + * Get whether to send a notification about the acknowledgement + * + * @return bool + */ + public function getNotify() + { + return $this->notify; + } + + /** + * Set whether the comment associated with the acknowledgement is persistent + * + * @param bool $persistent + * + * @return $this + */ + public function setPersistent($persistent = true) + { + $this->persistent = (bool) $persistent; + + return $this; + } + + /** + * Is the comment associated with the acknowledgement is persistent? + * + * @return bool + */ + public function getPersistent() + { + return $this->persistent; + } + + /** + * Set the time when the acknowledgement should expire + * + * @param int $expireTime + * + * @return $this + */ + public function setExpireTime($expireTime) + { + $this->expireTime = (int) $expireTime; + + return $this; + } + + /** + * Get the time when the acknowledgement should expire + * + * @return int + */ + public function getExpireTime() + { + return $this->expireTime; + } +} diff --git a/library/Icingadb/Command/Object/AddCommentCommand.php b/library/Icingadb/Command/Object/AddCommentCommand.php new file mode 100644 index 00000000..d8171337 --- /dev/null +++ b/library/Icingadb/Command/Object/AddCommentCommand.php @@ -0,0 +1,42 @@ +expireTime = (int) $expireTime; + + return $this; + } + + /** + * Get the time when the acknowledgement should expire + * + * @return int + */ + public function getExpireTime() + { + return $this->expireTime; + } +} diff --git a/library/Icingadb/Command/Object/CommandAuthor.php b/library/Icingadb/Command/Object/CommandAuthor.php new file mode 100644 index 00000000..fc8596ec --- /dev/null +++ b/library/Icingadb/Command/Object/CommandAuthor.php @@ -0,0 +1,39 @@ +author = (string) $author; + + return $this; + } + + /** + * Get the author + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } +} diff --git a/library/Icingadb/Command/Object/DeleteCommentCommand.php b/library/Icingadb/Command/Object/DeleteCommentCommand.php new file mode 100644 index 00000000..c30802e3 --- /dev/null +++ b/library/Icingadb/Command/Object/DeleteCommentCommand.php @@ -0,0 +1,46 @@ +commentName; + } + + /** + * Set the name of the comment + * + * @param string $commentName + * + * @return $this + */ + public function setCommentName($commentName) + { + $this->commentName = $commentName; + + return $this; + } +} diff --git a/library/Icingadb/Command/Object/DeleteDowntimeCommand.php b/library/Icingadb/Command/Object/DeleteDowntimeCommand.php new file mode 100644 index 00000000..6cb1b8d3 --- /dev/null +++ b/library/Icingadb/Command/Object/DeleteDowntimeCommand.php @@ -0,0 +1,52 @@ +downtimeName; + } + + /** + * Set the name of the downtime (Icinga 2.4+) + * + * Required for removing the downtime via Icinga 2's API. + * + * @param string $downtimeName + * + * @return $this + */ + public function setDowntimeName($downtimeName) + { + $this->downtimeName = $downtimeName; + + return $this; + } +} diff --git a/library/Icingadb/Command/Object/ObjectCommand.php b/library/Icingadb/Command/Object/ObjectCommand.php new file mode 100644 index 00000000..dd252480 --- /dev/null +++ b/library/Icingadb/Command/Object/ObjectCommand.php @@ -0,0 +1,45 @@ +object = $object; + + return $this; + } + + /** + * Get the involved object + * + * @return Model + */ + public function getObject() + { + return $this->object; + } +} diff --git a/library/Icingadb/Command/Object/ProcessCheckResultCommand.php b/library/Icingadb/Command/Object/ProcessCheckResultCommand.php new file mode 100644 index 00000000..532b77be --- /dev/null +++ b/library/Icingadb/Command/Object/ProcessCheckResultCommand.php @@ -0,0 +1,134 @@ +status = (int) $status; + + return $this; + } + + /** + * Get the status code of the host or service check result + * + * @return int + */ + public function getStatus() + { + return $this->status; + } + + /** + * Set the text output of the host or service check result + * + * @param string $output + * + * @return $this + */ + public function setOutput($output) + { + $this->output = (string) $output; + + return $this; + } + + /** + * Get the text output of the host or service check result + * + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Set the performance data of the host or service check result + * + * @param string $performanceData + * + * @return $this + */ + public function setPerformanceData($performanceData) + { + $this->performanceData = (string) $performanceData; + + return $this; + } + + /** + * Get the performance data of the host or service check result + * + * @return string + */ + public function getPerformanceData() + { + return $this->performanceData; + } +} diff --git a/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php b/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php new file mode 100644 index 00000000..0b009760 --- /dev/null +++ b/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php @@ -0,0 +1,42 @@ +triggered = (bool) $triggered; + + return $this; + } + + /** + * Get whether the downtime for child hosts are all set to be triggered by this' host downtime + * + * @return bool + */ + public function getTriggered() + { + return $this->triggered; + } +} diff --git a/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php b/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php new file mode 100644 index 00000000..49a22a30 --- /dev/null +++ b/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php @@ -0,0 +1,13 @@ +checkTime = (int) $checkTime; + + return $this; + } + + /** + * Get the time when the next check of a host or service is to be scheduled + * + * @return int Unix timestamp + */ + public function getCheckTime() + { + return $this->checkTime; + } + + /** + * Set whether the check is forced + * + * @param bool $forced + * + * @return $this + */ + public function setForced($forced = true) + { + $this->forced = (bool) $forced; + + return $this; + } + + /** + * Get whether the check is forced + * + * @return bool + */ + public function getForced() + { + return $this->forced; + } +} diff --git a/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php b/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php new file mode 100644 index 00000000..d6aafa56 --- /dev/null +++ b/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php @@ -0,0 +1,42 @@ +forAllServices = (bool) $forAllServices; + + return $this; + } + + /** + * Get whether to schedule a downtime for all services associated with a particular host + * + * @return bool + */ + public function getForAllServices() + { + return $this->forAllServices; + } +} diff --git a/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php b/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php new file mode 100644 index 00000000..51d0a655 --- /dev/null +++ b/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php @@ -0,0 +1,184 @@ +start = (int) $start; + + return $this; + } + + /** + * Get the time when the downtime should start + * + * @return int Unix timestamp + */ + public function getStart() + { + return $this->start; + } + + /** + * Set the time when the downtime should end + * + * @param int $end Unix timestamp + * + * @return $this + */ + public function setEnd($end) + { + $this->end = (int) $end; + + return $this; + } + + /** + * Get the time when the downtime should end + * + * @return int Unix timestamp + */ + public function getEnd() + { + return $this->end; + } + + /** + * Set whether it's a fixed or flexible downtime + * + * @param boolean $fixed + * + * @return $this + */ + public function setFixed($fixed = true) + { + $this->fixed = (bool) $fixed; + + return $this; + } + + /** + * Is the downtime fixed? + * + * @return boolean + */ + public function getFixed() + { + return $this->fixed; + } + + /** + * Set the ID of the downtime which triggers this downtime + * + * @param int $triggerId + * + * @return $this + */ + public function setTriggerId($triggerId) + { + $this->triggerId = (int) $triggerId; + + return $this; + } + + /** + * Get the ID of the downtime which triggers this downtime + * + * @return int|null + */ + public function getTriggerId() + { + return $this->triggerId; + } + + /** + * Set the duration in seconds the downtime must last if it's a flexible downtime + * + * @param int $duration + * + * @return $this + */ + public function setDuration($duration) + { + $this->duration = (int) $duration; + + return $this; + } + + /** + * Get the duration in seconds the downtime must last if it's a flexible downtime + * + * @return int|null + */ + public function getDuration() + { + return $this->duration; + } + + public function getName() + { + return 'ScheduleDowntime'; + } +} diff --git a/library/Icingadb/Command/Object/SendCustomNotificationCommand.php b/library/Icingadb/Command/Object/SendCustomNotificationCommand.php new file mode 100644 index 00000000..af225939 --- /dev/null +++ b/library/Icingadb/Command/Object/SendCustomNotificationCommand.php @@ -0,0 +1,44 @@ +forced; + } + + /** + * Set whether to force the notification + * + * @param bool $forced + * + * @return $this + */ + public function setForced($forced = true) + { + $this->forced = $forced; + + return $this; + } +} diff --git a/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php b/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php new file mode 100644 index 00000000..e54dbdb5 --- /dev/null +++ b/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php @@ -0,0 +1,102 @@ +feature = (string) $feature; + + return $this; + } + + /** + * Get the feature that is to be enabled or disabled + * + * @return string + */ + public function getFeature() + { + return $this->feature; + } + + /** + * Set whether the feature should be enabled or disabled + * + * @param bool $enabled + * + * @return $this + */ + public function setEnabled($enabled = true) + { + $this->enabled = (bool) $enabled; + + return $this; + } + + /** + * Get whether the feature should be enabled or disabled + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } +} diff --git a/library/Icingadb/Command/Object/WithCommentCommand.php b/library/Icingadb/Command/Object/WithCommentCommand.php new file mode 100644 index 00000000..fbc4a0dc --- /dev/null +++ b/library/Icingadb/Command/Object/WithCommentCommand.php @@ -0,0 +1,44 @@ +comment = (string) $comment; + + return $this; + } + + /** + * Get the comment + * + * @return string + */ + public function getComment() + { + return $this->comment; + } +} diff --git a/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php b/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php new file mode 100644 index 00000000..12f48ba5 --- /dev/null +++ b/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php @@ -0,0 +1,305 @@ +app; + } + + /** + * Set the name of the Icinga application object + * + * @param string $app + * + * @return $this + */ + public function setApp($app) + { + $this->app = $app; + + return $this; + } + + /** + * Apply filter to query data + * + * @param array $data + * @param Model $object + * + * @return array + */ + protected function applyFilter(array &$data, Model $object) + { + if ($object instanceof Host) { + $data['host'] = $object->name; + } else { + /** @var Service $object */ + $data['service'] = sprintf('%s!%s', $object->host->name, $object->name); + } + } + + /** + * Render a command + * + * @param IcingaCommand $command + * + * @return IcingaApiCommand + */ + public function render(IcingaCommand $command) + { + $renderMethod = 'render' . $command->getName(); + if (! method_exists($this, $renderMethod)) { + throw new InvalidArgumentException( + sprintf('Can\'t render command. Method %s not found', $renderMethod) + ); + } + + return $this->$renderMethod($command); + } + + public function renderAddComment(AddCommentCommand $command) + { + $endpoint = 'actions/add-comment'; + $data = [ + 'author' => $command->getAuthor(), + 'comment' => $command->getComment() + ]; + + if ($command->getExpireTime() !== null) { + $data['expiry'] = $command->getExpireTime(); + } + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderSendCustomNotification(SendCustomNotificationCommand $command) + { + $endpoint = 'actions/send-custom-notification'; + $data = [ + 'author' => $command->getAuthor(), + 'comment' => $command->getComment(), + 'force' => $command->getForced() + ]; + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderProcessCheckResult(ProcessCheckResultCommand $command) + { + $endpoint = 'actions/process-check-result'; + $data = [ + 'exit_status' => $command->getStatus(), + 'plugin_output' => $command->getOutput(), + 'performance_data' => $command->getPerformanceData() + ]; + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderScheduleCheck(ScheduleCheckCommand $command) + { + $endpoint = 'actions/reschedule-check'; + $data = [ + 'next_check' => $command->getCheckTime(), + 'force' => $command->getForced() + ]; + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderScheduleDowntime(ScheduleServiceDowntimeCommand $command) + { + $endpoint = 'actions/schedule-downtime'; + $data = [ + 'author' => $command->getAuthor(), + 'comment' => $command->getComment(), + 'start_time' => $command->getStart(), + 'end_time' => $command->getEnd(), + 'duration' => $command->getDuration(), + 'fixed' => $command->getFixed(), + 'trigger_name' => $command->getTriggerId() + ]; + + if ($command instanceof PropagateHostDowntimeCommand) { + $data['child_options'] = $command->getTriggered() ? 1 : 2; + } + + if ($command instanceof ScheduleHostDowntimeCommand && $command->getForAllServices()) { + $data['all_services'] = true; + } + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command) + { + $endpoint = 'actions/acknowledge-problem'; + $data = [ + 'author' => $command->getAuthor(), + 'comment' => $command->getComment(), + 'sticky' => $command->getSticky(), + 'notify' => $command->getNotify(), + 'persistent' => $command->getPersistent() + ]; + + if ($command->getExpireTime() !== null) { + $data['expiry'] = $command->getExpireTime(); + } + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command) + { + switch ($command->getFeature()) { + case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS: + $attr = 'enable_active_checks'; + break; + case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS: + $attr = 'enable_passive_checks'; + break; + case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS: + $attr = 'enable_notifications'; + break; + case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER: + $attr = 'enable_event_handler'; + break; + case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION: + $attr = 'enable_flapping'; + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + + $endpoint = 'objects/'; + $object = $command->getObject(); + if ($object instanceof Host) { + $endpoint .= 'hosts'; + } else { + /** @var Service $object */ + $endpoint .= 'services'; + } + + $data = [ + 'attrs' => [ + $attr => $command->getEnabled() + ] + ]; + + $this->applyFilter($data, $object); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderDeleteComment(DeleteCommentCommand $command) + { + $endpoint = 'actions/remove-comment'; + $data = [ + 'author' => $command->getAuthor(), + 'comment' => $command->getCommentName() + ]; + + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderDeleteDowntime(DeleteDowntimeCommand $command) + { + $endpoint = 'actions/remove-downtime'; + $data = [ + 'author' => $command->getAuthor(), + 'downtime' => $command->getDowntimeName() + ]; + + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command) + { + $endpoint = 'actions/remove-acknowledgement'; + $data = ['author' => $command->getAuthor()]; + + $this->applyFilter($data, $command->getObject()); + return IcingaApiCommand::create($endpoint, $data); + } + + public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command) + { + $endpoint = 'objects/icingaapplications/' . $this->getApp(); + + switch ($command->getFeature()) { + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: + $attr = 'enable_host_checks'; + break; + case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: + $attr = 'enable_service_checks'; + break; + case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: + $attr = 'enable_event_handlers'; + break; + case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: + $attr = 'enable_flapping'; + break; + case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: + $attr = 'enable_notifications'; + break; + case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: + $attr = 'enable_perfdata'; + break; + default: + throw new InvalidArgumentException($command->getFeature()); + } + + $data = [ + 'attrs' => [ + $attr => $command->getEnabled() + ] + ]; + + return IcingaApiCommand::create($endpoint, $data); + } +} diff --git a/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php b/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php new file mode 100644 index 00000000..50dd90c1 --- /dev/null +++ b/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php @@ -0,0 +1,12 @@ +renderer = new IcingaApiCommandRenderer(); + } + + /** + * Set the name of the Icinga application object + * + * @param string $app + * + * @return $this + */ + public function setApp($app) + { + $this->renderer->setApp($app); + + return $this; + } + + /** + * Get the API host + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Set the API host + * + * @param string $host + * + * @return $this + */ + public function setHost($host) + { + $this->host = $host; + + return $this; + } + + /** + * Get the API password + * + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Set the API password + * + * @param string $password + * + * @return $this + */ + public function setPassword($password) + { + $this->password = $password; + + return $this; + } + + /** + * Get the API port + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Set the API port + * + * @param int $port + * + * @return $this + */ + public function setPort($port) + { + $this->port = (int) $port; + + return $this; + } + + /** + * Get the API username + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Set the API username + * + * @param string $username + * + * @return $this + */ + public function setUsername($username) + { + $this->username = $username; + + return $this; + } + + /** + * Get URI for endpoint + * + * @param string $endpoint + * + * @return string + */ + protected function getUriFor($endpoint) + { + return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint); + } + + protected function sendCommand(IcingaApiCommand $command) + { + Logger::debug( + 'Sending Icinga command "%s" to the API "%s:%u"', + $command->getEndpoint(), + $this->getHost(), + $this->getPort() + ); + + $data = $command->getData(); + $payload = Json::encode($data); + AuditHook::logActivity( + 'monitoring/command', + "Issued command {$command->getEndpoint()} with the following payload: $payload", + $data + ); + + try { + $response = (new Client()) + ->post($this->getUriFor($command->getEndpoint()), [ + 'auth' => [$this->getUsername(), $this->getPassword()], + 'headers' => ['Accept' => 'application/json'], + 'json' => $command->getData(), + 'http_errors' => false, + 'verify' => false + ]); + } catch (GuzzleException $e) { + throw new CommandTransportException( + 'Can\'t connect to the Icinga 2 API: %u %s', + $e->getCode(), + $e->getMessage() + ); + } + + try { + $responseData = Json::decode((string) $response->getBody(), true); + } catch (JsonDecodeException $e) { + throw new CommandTransportException( + 'Got invalid JSON response from the Icinga 2 API: %s', + $e->getMessage() + ); + } + + if (! isset($responseData['results']) || empty($responseData['results'])) { + return; + } + + $result = array_pop($responseData['results']); + if ($result['code'] < 200 || $result['code'] >= 300) { + throw new ApiCommandException( + 'Can\'t send external Icinga command: %u %s', + $result['code'], + $result['status'] + ); + } + } + + /** + * Send the Icinga command over the Icinga 2 API + * + * @param IcingaCommand $command + * @param int|null $now + * + * @throws CommandTransportException + */ + public function send(IcingaCommand $command, $now = null) + { + $this->sendCommand($this->renderer->render($command)); + } + + /** + * Try to connect to the API + * + * @throws CommandTransportException In case the connection was not successful + */ + public function probe() + { + try { + $response = (new Client()) + ->get($this->getUriFor(null), [ + 'auth' => [$this->getUsername(), $this->getPassword()], + 'headers' => ['Accept' => 'application/json'], + 'http_errors' => false, + 'verify' => false + ]); + } catch (GuzzleException $e) { + throw new CommandTransportException( + 'Can\'t connect to the Icinga 2 API: %u %s', + $e->getCode(), + $e->getMessage() + ); + } + + try { + $responseData = Json::decode((string) $response->getBody(), true); + } catch (JsonDecodeException $e) { + throw new CommandTransportException( + 'Got invalid JSON response from the Icinga 2 API: %s', + $e->getMessage() + ); + } + + if (! isset($responseData['results']) || empty($responseData['results'])) { + throw new CommandTransportException( + 'Got invalid response from the Icinga 2 API: %s', + JSON::encode($responseData) + ); + } + + $result = array_pop($responseData['results']); + if (! isset($result['user']) || $result['user'] !== $this->getUsername()) { + throw new CommandTransportException( + 'Got invalid response from the Icinga 2 API: %s', + JSON::encode($responseData) + ); + } + } +} diff --git a/library/Icingadb/Command/Transport/CommandTransport.php b/library/Icingadb/Command/Transport/CommandTransport.php new file mode 100644 index 00000000..99a1bb6a --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransport.php @@ -0,0 +1,128 @@ +isEmpty()) { + throw new ConfigurationError( + t('No command transports have been configured in "%s".'), + $config->getConfigFile() + ); + } + + static::$config = $config; + } + + return static::$config; + } + + /** + * Create a transport from config + * + * @param ConfigObject $config + * + * @return ApiCommandTransport + * + * @throws ConfigurationError + */ + public static function createTransport(ConfigObject $config) + { + $config = clone $config; + switch (strtolower($config->transport)) { + case ApiCommandTransport::TRANSPORT: + $transport = new ApiCommandTransport(); + break; + default: + throw new ConfigurationError( + t('Cannot create command transport "%s". Invalid transport defined in "%s". Use one of: %s.'), + $config->transport, + static::getConfig()->getConfigFile(), + join(', ', [ApiCommandTransport::TRANSPORT]) + ); + } + + unset($config->transport); + foreach ($config as $key => $value) { + $method = 'set' . ucfirst($key); + if (! method_exists($transport, $method)) { + // Ignore settings from config that don't have a setter on the transport instead of throwing an + // exception here because the transport should throw an exception if it's not fully set up + // when being about to send a command + continue; + } + + $transport->$method($value); + } + + return $transport; + } + + /** + * Send the given command over an appropriate Icinga command transport + * + * This will try one configured transport after another until the command has been successfully sent. + * + * @param IcingaCommand $command The command to send + * @param int|null $now Timestamp of the command or null for now + * + * @throws CommandTransportException If sending the Icinga command failed + */ + public function send(IcingaCommand $command, $now = null) + { + $errors = []; + + foreach (static::getConfig() as $name => $transportConfig) { + $transport = static::createTransport($transportConfig); + + try { + $transport->send($command, $now); + } catch (CommandTransportException $e) { + Logger::error($e); + $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.')); + continue; // Try the next transport + } + + return; // The command was successfully sent + } + + if (! empty($errors)) { + throw new CommandTransportException(implode("\n", $errors)); + } + + throw new CommandTransportException(t( + 'Failed to send external Icinga command. No transport has been configured' + . ' for this instance. Please contact your Icinga Web administrator.' + )); + } +} diff --git a/library/Icingadb/Command/Transport/CommandTransportConfig.php b/library/Icingadb/Command/Transport/CommandTransportConfig.php new file mode 100644 index 00000000..e17fa041 --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransportConfig.php @@ -0,0 +1,31 @@ + [ + 'name' => 'commandtransports', + 'module' => 'icingadb', + 'keyColumn' => 'name' + ] + ]; + + protected $queryColumns = [ + 'transport' => [ + 'name', + 'transport', + + // API options + 'host', + 'port', + 'username', + 'password' + ] + ]; +} diff --git a/library/Icingadb/Command/Transport/CommandTransportException.php b/library/Icingadb/Command/Transport/CommandTransportException.php new file mode 100644 index 00000000..2ca89d9d --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransportException.php @@ -0,0 +1,14 @@ +getItemClass(); + + $i = 0; + foreach ($this->data as $data) { + $item = new $itemClass($data, $this); + $item->setOrder($i++); + + $this->add($item); + } + + if ($this->isEmpty()) { + $this->setTag('div'); + $this->add(new EmptyState(t('No items found.'))); + } + } +} diff --git a/library/Icingadb/Common/BaseOrderedListItem.php b/library/Icingadb/Common/BaseOrderedListItem.php new file mode 100644 index 00000000..87d5ab74 --- /dev/null +++ b/library/Icingadb/Common/BaseOrderedListItem.php @@ -0,0 +1,37 @@ +order = (int) $order; + + return $this; + } + + /** + * Get this element's position + * + * @return int + */ + public function getOrder() + { + return $this->order; + } +} diff --git a/library/Icingadb/Common/CommandActions.php b/library/Icingadb/Common/CommandActions.php index 0ac67f71..12ca549e 100644 --- a/library/Icingadb/Common/CommandActions.php +++ b/library/Icingadb/Common/CommandActions.php @@ -4,27 +4,18 @@ namespace Icinga\Module\Icingadb\Common; -use Icinga\Module\Icingadb\Compat\CompatBackend; -use Icinga\Module\Icingadb\Compat\CompatHost; -use Icinga\Module\Icingadb\Compat\CompatObjects; -use Icinga\Module\Icingadb\Compat\CompatService; -use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentsCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimesCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostCheckCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceCheckCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm; -use ipl\Html\HtmlString; +use GuzzleHttp\Psr7\ServerRequest; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Module\Icingadb\Forms\Command\Object\AcknowledgeProblemForm; +use Icinga\Module\Icingadb\Forms\Command\Object\AddCommentForm; +use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; +use Icinga\Module\Icingadb\Forms\Command\Object\ProcessCheckResultForm; +use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; +use Icinga\Module\Icingadb\Forms\Command\Object\ScheduleCheckForm; +use Icinga\Module\Icingadb\Forms\Command\Object\ScheduleHostDowntimeForm; +use Icinga\Module\Icingadb\Forms\Command\Object\ScheduleServiceDowntimeForm; +use Icinga\Module\Icingadb\Forms\Command\Object\SendCustomNotificationForm; +use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Web\Url; @@ -82,118 +73,80 @@ trait CommandActions return $this->commandTargetModel; } - /** - * Get command objects - * - * @return CompatObjects - */ - protected function getCommandObjects() - { - switch ($this->getCommandTargetModel()->getTableName()) { - case 'host': - $compatClass = CompatHost::class; - break; - case 'service': - $compatClass = CompatService::class; - break; - default: - throw new LogicException('Only hosts and services are supported'); - } - - return new CompatObjects($this->getCommandTargets(), $compatClass); - } - /** * Handle and register the given command form * - * @param string|ObjectsCommandForm $form + * @param string|CommandForm $form */ protected function handleCommandForm($form) { if (is_string($form)) { - $form = new $form([ - 'backend' => new CompatBackend(), - 'objects' => $this->getCommandObjects() - ]); + /** @var \Icinga\Module\Icingadb\Forms\Command\CommandForm $form */ + $form = new $form(); } - $form->setRedirectUrl($this->getCommandTargetsUrl()); + $actionUrl = $this->getRequest()->getUrl(); + if ($this->view->compact) { + $actionUrl = clone $actionUrl; + // TODO: This solves https://github.com/Icinga/icingadb-web/issues/124 but I'd like to omit this + // entirely. I think it should be solved like https://github.com/Icinga/icingaweb2/pull/4300 so + // that a request's url object still has params like showCompact and _dev + $actionUrl->getParams()->add('showCompact', true); + } - $form->handleRequest(); - $this->addContent(HtmlString::create($form->render())); + $form->setAction($actionUrl->getAbsoluteUrl()); + $form->setObjects($this->getCommandTargets()); + $form->on($form::ON_SUCCESS, function () { + // This forces the column to reload nearly instantly after the redirect + // and ensures the effect of the command is visible to the user asap + $this->getResponse()->setAutoRefreshInterval(1); + + $this->redirectNow($this->getCommandTargetsUrl()); + }); + + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->addContent($form); } public function acknowledgeAction() { $this->assertPermission('monitoring/command/acknowledge-problem'); $this->setTitle(t('Acknowledge Problem')); - $this->handleCommandForm(AcknowledgeProblemCommandForm::class); + $this->handleCommandForm(AcknowledgeProblemForm::class); } public function addCommentAction() { $this->assertPermission('monitoring/command/comment/add'); $this->setTitle(t('Add Comment')); - $this->handleCommandForm(AddCommentCommandForm::class); + $this->handleCommandForm(AddCommentForm::class); } public function checkNowAction() { $this->assertPermission('monitoring/command/schedule-check'); - $this->handleCommandForm(CheckNowCommandForm::class); - } - - public function deleteCommentAction() - { - $this->assertPermission('monitoring/command/comment/delete'); - $this->handleCommandForm(DeleteCommentCommandForm::class); - } - - public function deleteCommentsAction() - { - $this->assertPermission('monitoring/command/comment/delete'); - $this->handleCommandForm(DeleteCommentsCommandForm::class); - } - - public function deleteDowntimeAction() - { - $this->assertPermission('monitoring/command/downtime/delete'); - $this->handleCommandForm(DeleteDowntimeCommandForm::class); - } - - public function deleteDowntimesAction() - { - $this->assertPermission('monitoring/command/downtime/delete'); - $this->handleCommandForm(DeleteDowntimesCommandForm::class); + $this->handleCommandForm(CheckNowForm::class); } public function processCheckresultAction() { $this->assertPermission('monitoring/command/process-check-result'); $this->setTitle(t('Submit Passive Check Result')); - $this->handleCommandForm(ProcessCheckResultCommandForm::class); + $this->handleCommandForm(ProcessCheckResultForm::class); } public function removeAcknowledgementAction() { $this->assertPermission('monitoring/command/remove-acknowledgement'); - $this->handleCommandForm(RemoveAcknowledgementCommandForm::class); + $this->handleCommandForm(RemoveAcknowledgementForm::class); } public function scheduleCheckAction() { $this->assertPermission('monitoring/command/schedule-check'); - - switch ($this->getCommandTargetModel()->getTableName()) { - case 'host': - $this->setTitle(t('Reschedule Host Check')); - $this->handleCommandForm(ScheduleHostCheckCommandForm::class); - break; - case 'service': - $this->setTitle(t('Reschedule Service Check')); - $this->handleCommandForm(ScheduleServiceCheckCommandForm::class); - break; - } + $this->setTitle(t('Reschedule Check')); + $this->handleCommandForm(ScheduleCheckForm::class); } public function scheduleDowntimeAction() @@ -203,11 +156,11 @@ trait CommandActions switch ($this->getCommandTargetModel()->getTableName()) { case 'host': $this->setTitle(t('Schedule Host Downtime')); - $this->handleCommandForm(ScheduleHostDowntimeCommandForm::class); + $this->handleCommandForm(ScheduleHostDowntimeForm::class); break; case 'service': $this->setTitle(t('Schedule Service Downtime')); - $this->handleCommandForm(ScheduleServiceDowntimeCommandForm::class); + $this->handleCommandForm(ScheduleServiceDowntimeForm::class); break; } } @@ -216,26 +169,22 @@ trait CommandActions { $this->assertPermission('monitoring/command/send-custom-notification'); $this->setTitle(t('Send Custom Notification')); - $this->handleCommandForm(SendCustomNotificationCommandForm::class); + $this->handleCommandForm(SendCustomNotificationForm::class); } public function toggleFeaturesAction() { - $commandObjects = $this->getCommandObjects(); - $form = new ToggleObjectFeaturesCommandForm([ - 'backend' => new CompatBackend(), - 'objects' => $commandObjects - ]); - + $commandObjects = $this->getCommandTargets(); if (count($commandObjects) > 1) { if (! method_exists($this, 'getFeatureStatus')) { throw new LogicException('You must implement getFeatureStatus() first'); } - $form->load($this->getFeatureStatus()); + $form = new ToggleObjectFeaturesForm($this->getFeatureStatus()); } else { foreach ($commandObjects as $object) { - $form->load($object); + // There's only a single result, a foreach is the most compatible way to retrieve the object + $form = new ToggleObjectFeaturesForm($object); } } diff --git a/library/Icingadb/Common/HostLinks.php b/library/Icingadb/Common/HostLinks.php index 15f0a8d4..b3d1f32e 100644 --- a/library/Icingadb/Common/HostLinks.php +++ b/library/Icingadb/Common/HostLinks.php @@ -29,11 +29,6 @@ abstract class HostLinks return Url::fromPath('icingadb/host/schedule-check', ['name' => $host->name]); } - public static function cancelDowntime(Host $host) - { - return Url::fromPath('icingadb/host/delete-downtime', ['name' => $host->name]); - } - public static function comments(Host $host) { return Url::fromPath('icingadb/host/comments', ['name' => $host->name]); @@ -54,11 +49,6 @@ abstract class HostLinks return Url::fromPath('icingadb/host/remove-acknowledgement', ['name' => $host->name]); } - public static function removeComment(Host $host) - { - return Url::fromPath('icingadb/host/delete-comment', ['name' => $host->name]); - } - public static function scheduleDowntime(Host $host) { return Url::fromPath('icingadb/host/schedule-downtime', ['name' => $host->name]); diff --git a/library/Icingadb/Common/ServiceLinks.php b/library/Icingadb/Common/ServiceLinks.php index 64dc35b0..fb4ea7dd 100644 --- a/library/Icingadb/Common/ServiceLinks.php +++ b/library/Icingadb/Common/ServiceLinks.php @@ -42,14 +42,6 @@ abstract class ServiceLinks ); } - public static function cancelDowntime(Service $service, Host $host) - { - return Url::fromPath( - 'icingadb/service/delete-downtime', - ['name' => $service->name, 'host.name' => $host->name] - ); - } - public static function comments(Service $service, Host $host) { return Url::fromPath( @@ -82,14 +74,6 @@ abstract class ServiceLinks ); } - public static function removeComment(Service $service, Host $host) - { - return Url::fromPath( - 'icingadb/service/delete-comment', - ['name' => $service->name, 'host.name' => $host->name] - ); - } - public static function scheduleDowntime(Service $service, Host $host) { return Url::fromPath( diff --git a/library/Icingadb/Compat/CompatBackend.php b/library/Icingadb/Compat/CompatBackend.php deleted file mode 100644 index 7a562a48..00000000 --- a/library/Icingadb/Compat/CompatBackend.php +++ /dev/null @@ -1,35 +0,0 @@ -query = $query; - $this->compatClass = $compatClass; - } - - public function getIterator() - { - foreach ($this->query as $object) { - yield new $this->compatClass($object); - } - } - - public function count() - { - if ($this->query instanceof Traversable) { - return $this->query->count(); - } - - return count($this->query); - } -} diff --git a/library/Icingadb/Compat/FeatureStatus.php b/library/Icingadb/Compat/FeatureStatus.php deleted file mode 100644 index 62c01fed..00000000 --- a/library/Icingadb/Compat/FeatureStatus.php +++ /dev/null @@ -1,44 +0,0 @@ - $this->getFeatureStatus('active_checks_enabled', $prefix, $summary), - 'passive_checks_enabled' => $this->getFeatureStatus('passive_checks_enabled', $prefix, $summary), - 'notifications_enabled' => $this->getFeatureStatus('notifications_enabled', $prefix, $summary), - 'event_handler_enabled' => $this->getFeatureStatus('event_handler_enabled', $prefix, $summary), - 'flap_detection_enabled' => $this->getFeatureStatus('flapping_enabled', $prefix, $summary) - ]; - - parent::__construct($featureStatus, ArrayObject::ARRAY_AS_PROPS); - } - - protected function getFeatureStatus($feature, $prefix, $summary) - { - $key = "{$prefix}_{$feature}"; - $value = (int) $summary->$key; - - if ($value === 0) { - return 0; - } - - $totalKey = "{$prefix}_total"; - $total = (int) $summary->$totalKey; - - if ($value === $total) { - return 1; - } - - return 2; - } -} diff --git a/library/Icingadb/Util/FeatureStatus.php b/library/Icingadb/Util/FeatureStatus.php new file mode 100644 index 00000000..7e22860a --- /dev/null +++ b/library/Icingadb/Util/FeatureStatus.php @@ -0,0 +1,50 @@ + + $this->getFeatureStatus('active_checks_enabled', $prefix, $summary), + ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => + $this->getFeatureStatus('passive_checks_enabled', $prefix, $summary), + ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => + $this->getFeatureStatus('notifications_enabled', $prefix, $summary), + ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => + $this->getFeatureStatus('event_handler_enabled', $prefix, $summary), + ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => + $this->getFeatureStatus('flapping_enabled', $prefix, $summary) + ]; + + parent::__construct($featureStatus, ArrayObject::ARRAY_AS_PROPS); + } + + protected function getFeatureStatus($feature, $prefix, $summary) + { + $key = "{$prefix}_{$feature}"; + $value = (int) $summary->$key; + + if ($value === 0) { + return 0; + } + + $totalKey = "{$prefix}_total"; + $total = (int) $summary->$totalKey; + + if ($value === $total) { + return 1; + } + + return 2; + } +} diff --git a/library/Icingadb/Widget/BaseItemList.php b/library/Icingadb/Widget/BaseItemList.php index 86b9da1d..454c0244 100644 --- a/library/Icingadb/Widget/BaseItemList.php +++ b/library/Icingadb/Widget/BaseItemList.php @@ -12,6 +12,8 @@ use ipl\Web\Url; /** * Base class for item lists + * + * @todo Move this to Icinga\Module\Icingadb\Common */ abstract class BaseItemList extends BaseHtmlElement { diff --git a/library/Icingadb/Widget/BaseListItem.php b/library/Icingadb/Widget/BaseListItem.php index e1b97227..cc7fa3d0 100644 --- a/library/Icingadb/Widget/BaseListItem.php +++ b/library/Icingadb/Widget/BaseListItem.php @@ -11,6 +11,8 @@ use ipl\Web\Filter\QueryString; /** * Base class for list items + * + * @todo Move this to Icinga\Module\Icingadb\Common */ abstract class BaseListItem extends BaseHtmlElement { diff --git a/library/Icingadb/Widget/Detail/CommentDetail.php b/library/Icingadb/Widget/Detail/CommentDetail.php index 66ab294a..3fd6d029 100644 --- a/library/Icingadb/Widget/Detail/CommentDetail.php +++ b/library/Icingadb/Widget/Detail/CommentDetail.php @@ -6,17 +6,13 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\HostLink; -use Icinga\Module\Icingadb\Common\HostLinks; +use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLink; -use Icinga\Module\Icingadb\Common\ServiceLinks; +use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm; use Icinga\Module\Icingadb\Widget\TimeUntil; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; -use ipl\Html\HtmlDocument; -use ipl\Html\HtmlString; -use ipl\Web\Widget\Icon; class CommentDetail extends BaseHtmlElement { @@ -70,32 +66,14 @@ class CommentDetail extends BaseHtmlElement protected function createRemoveCommentForm() { - $formData = [ - 'comment_id' => $this->comment->name, - 'comment_name' => $this->comment->name, - 'redirect' => '__BACK__' - ]; + // TODO: Check permission + $action = Links::commentsDelete(); + $action->setParam('name', $this->comment->name); - - if ($this->comment->object_type === 'host') { - $action = HostLinks::removeComment($this->comment->host); - } else { - $action = ServiceLinks::removeComment($this->comment->service, $this->comment->service->host); - $formData['comment_is_service'] = true; - } - - $removeCommentForm = (new DeleteCommentCommandForm()) - ->create() - ->populate($formData) - ->setAction($action); - - $submitButton = $removeCommentForm->getElement('btn_submit'); - $submitButton->content = (new HtmlDocument()) - ->add([new Icon('trash'), t('Remove Comment')]) - ->setSeparator(' ') - ->render(); - - return new HtmlString($removeCommentForm->render()); + return (new DeleteCommentForm()) + ->setObjects([$this->comment]) + ->populate(['redirect' => '__BACK__']) + ->setAction($action->getAbsoluteUrl()); } protected function assemble() diff --git a/library/Icingadb/Widget/Detail/DowntimeDetail.php b/library/Icingadb/Widget/Detail/DowntimeDetail.php index d6d21c97..ab173a4f 100644 --- a/library/Icingadb/Widget/Detail/DowntimeDetail.php +++ b/library/Icingadb/Widget/Detail/DowntimeDetail.php @@ -8,13 +8,12 @@ use Icinga\Date\DateFormatter; use Icinga\Date\DateFormatter as WebDateFormatter; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\HostLink; -use Icinga\Module\Icingadb\Common\HostLinks; +use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLink; -use Icinga\Module\Icingadb\Common\ServiceLinks; +use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Widget\HorizontalKeyValue; -use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlDocument; @@ -73,31 +72,14 @@ class DowntimeDetail extends BaseHtmlElement protected function createCancelDowntimeForm() { - $formData = [ - 'downtime_id' => $this->downtime->name, - 'downtime_name' => $this->downtime->name, - 'redirect' => '__BACK__' - ]; + // TODO: Check permission + $action = Links::downtimesDelete(); + $action->setParam('name', $this->downtime->name); - if ($this->downtime->object_type === 'host') { - $action = HostLinks::cancelDowntime($this->downtime->host); - } else { - $action = ServiceLinks::cancelDowntime($this->downtime->service, $this->downtime->service->host); - $formData['downtime_is_service'] = true; - } - - $cancelDowntimeForm = (new DeleteDowntimeCommandForm()) - ->create() - ->populate($formData) - ->setAction($action); - - $submitButton = $cancelDowntimeForm->getElement('btn_submit'); - $submitButton->content = (new HtmlDocument()) - ->add([new Icon('trash'), t('Cancel Downtime')]) - ->setSeparator(' ') - ->render(); - - return new HtmlString($cancelDowntimeForm->render()); + return (new DeleteDowntimeForm()) + ->setObjects([$this->downtime]) + ->populate(['redirect' => '__BACK__']) + ->setAction($action->getAbsoluteUrl()); } protected function createTimeline() diff --git a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php index bd488744..6f11c287 100644 --- a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php +++ b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php @@ -6,11 +6,10 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\BaseFilter; -use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm; +use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; +use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; -use ipl\Html\HtmlString; use ipl\Web\Filter\QueryString; use ipl\Web\Url; @@ -55,12 +54,11 @@ class MultiselectQuickActions extends BaseHtmlElement $this->summary->$acks > 0 && $this->getAuth()->hasPermission('monitoring/command/remove-acknowledgement') ) { - $removeAckForm = (new RemoveAcknowledgementCommandForm()) + $removeAckForm = (new RemoveAcknowledgementForm()) ->setAction($this->getLink('removeAcknowledgement')) - ->setLabelEnabled(true) - ->setObjects([true]); + ->setObjects(array_fill(0, $this->summary->$acks, null)); - $this->add(Html::tag('li', new HtmlString($removeAckForm->render()))); + $this->add(Html::tag('li', $removeAckForm)); } if ( @@ -70,10 +68,7 @@ class MultiselectQuickActions extends BaseHtmlElement && $this->getAuth()->hasPermission('monitoring/command/schedule-check/active-only') ) ) { - $checkNowForm = (new CheckNowCommandForm()) - ->setAction($this->getLink('checkNow')); - - $this->add(Html::tag('li', new HtmlString($checkNowForm->render()))); + $this->add(Html::tag('li', (new CheckNowForm())->setAction($this->getLink('checkNow')))); } if ($this->getAuth()->hasPermission('monitoring/command/comment/add')) { @@ -151,6 +146,7 @@ class MultiselectQuickActions extends BaseHtmlElement protected function getLink($action) { return Url::fromPath("icingadb/{$this->type}s/$action") - ->setQueryString(QueryString::render($this->getBaseFilter())); + ->setQueryString(QueryString::render($this->getBaseFilter())) + ->getAbsoluteUrl(); } } diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index 29edecf5..52574ccb 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -12,12 +12,10 @@ use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLinks; -use Icinga\Module\Icingadb\Compat\CompatBackend; -use Icinga\Module\Icingadb\Compat\CompatHost; use Icinga\Module\Icingadb\Compat\CompatObject; use Icinga\Module\Icingadb\Compat\CompatPluginOutput; -use Icinga\Module\Icingadb\Compat\CompatService; use Icinga\Module\Icingadb\Compat\CustomvarFilter; +use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Widget\DowntimeList; use Icinga\Module\Icingadb\Widget\EmptyState; @@ -25,7 +23,6 @@ use Icinga\Module\Icingadb\Widget\HorizontalKeyValue; use Icinga\Module\Icingadb\Widget\ItemList\CommentList; use Icinga\Module\Icingadb\Widget\ShowMore; use Icinga\Module\Icingadb\Widget\TagList; -use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm; use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; use Icinga\Module\Monitoring\Hook\ObjectActionsHook; use Icinga\Web\Helper\Markdown; @@ -364,21 +361,17 @@ class ObjectDetail extends BaseHtmlElement protected function createFeatureToggles() { - $form = new ToggleObjectFeaturesCommandForm([ - 'backend' => new CompatBackend() - ]); + $form = new ToggleObjectFeaturesForm($this->object); if ($this->objectType === 'host') { - $form->load(new CompatHost($this->object)); - $form->setAction(HostLinks::toggleFeatures($this->object)); + $form->setAction(HostLinks::toggleFeatures($this->object)->getAbsoluteUrl()); } else { - $form->load(new CompatService($this->object)); - $form->setAction(ServiceLinks::toggleFeatures($this->object, $this->object->host)); + $form->setAction(ServiceLinks::toggleFeatures($this->object, $this->object->host)->getAbsoluteUrl()); } return [ Html::tag('h2', t('Feature Commands')), - HtmlString::create($form->render()) + $form ]; } diff --git a/library/Icingadb/Widget/Detail/ObjectsDetail.php b/library/Icingadb/Widget/Detail/ObjectsDetail.php index f875340c..968f157f 100644 --- a/library/Icingadb/Widget/Detail/ObjectsDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectsDetail.php @@ -7,13 +7,12 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Common\BaseFilter; use Icinga\Module\Icingadb\Common\Links; -use Icinga\Module\Icingadb\Compat\CompatBackend; -use Icinga\Module\Icingadb\Compat\FeatureStatus; +use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; +use Icinga\Module\Icingadb\Util\FeatureStatus; use Icinga\Module\Icingadb\Widget\EmptyState; use Icinga\Module\Icingadb\Widget\HostStateBadges; use Icinga\Module\Icingadb\Widget\ServiceStateBadges; use Icinga\Module\Icingadb\Widget\VerticalKeyValue; -use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlString; @@ -127,25 +126,25 @@ class ObjectsDetail extends BaseHtmlElement protected function createFeatureToggles() { - $form = new ToggleObjectFeaturesCommandForm([ - 'backend' => new CompatBackend() - ]); - - $form->load(new FeatureStatus($this->type, $this->summary)); + $form = new ToggleObjectFeaturesForm(new FeatureStatus($this->type, $this->summary)); if ($this->type === 'host') { $form->setAction( - Links::toggleHostsFeatures()->setQueryString(QueryString::render($this->getBaseFilter())) + Links::toggleHostsFeatures() + ->setQueryString(QueryString::render($this->getBaseFilter())) + ->getAbsoluteUrl() ); } else { $form->setAction( - Links::toggleServicesFeatures()->setQueryString(QueryString::render($this->getBaseFilter())) + Links::toggleServicesFeatures() + ->setQueryString(QueryString::render($this->getBaseFilter())) + ->getAbsoluteUrl() ); } return [ Html::tag('h2', t('Feature Commands')), - HtmlString::create($form->render()) + $form ]; } diff --git a/library/Icingadb/Widget/Detail/QuickActions.php b/library/Icingadb/Widget/Detail/QuickActions.php index 607245cf..7f21cb3b 100644 --- a/library/Icingadb/Widget/Detail/QuickActions.php +++ b/library/Icingadb/Widget/Detail/QuickActions.php @@ -7,13 +7,12 @@ namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Common\ServiceLinks; +use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; +use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; -use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm; -use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; -use ipl\Html\HtmlString; use ipl\Web\Widget\Icon; class QuickActions extends BaseHtmlElement @@ -37,12 +36,11 @@ class QuickActions extends BaseHtmlElement if ($this->object->state->is_problem) { if ($this->object->state->is_acknowledged) { if ($this->getAuth()->hasPermission('monitoring/command/remove-acknowledgement')) { - $removeAckForm = (new RemoveAcknowledgementCommandForm()) + $removeAckForm = (new RemoveAcknowledgementForm()) ->setAction($this->getLink('removeAcknowledgement')) - ->setLabelEnabled(true) - ->setObjects([true]); + ->setObjects([$this->object]); - $this->add(Html::tag('li', new HtmlString($removeAckForm->render()))); + $this->add(Html::tag('li', $removeAckForm)); } } elseif ($this->getAuth()->hasPermission('monitoring/command/acknowledge-problem')) { $this->assembleAction( @@ -61,10 +59,7 @@ class QuickActions extends BaseHtmlElement && $this->getAuth()->hasPermission('monitoring/command/schedule-check/active-only') ) ) { - $checkNowForm = (new CheckNowCommandForm()) - ->setAction($this->getLink('checkNow')); - - $this->add(Html::tag('li', new HtmlString($checkNowForm->render()))); + $this->add(Html::tag('li', (new CheckNowForm())->setAction($this->getLink('checkNow')))); } if ($this->getAuth()->hasPermission('monitoring/command/comment/add')) { @@ -145,9 +140,9 @@ class QuickActions extends BaseHtmlElement protected function getLink($action) { if ($this->object instanceof Host) { - return HostLinks::$action($this->object); + return HostLinks::$action($this->object)->getAbsoluteUrl(); } else { - return ServiceLinks::$action($this->object, $this->object->host); + return ServiceLinks::$action($this->object, $this->object->host)->getAbsoluteUrl(); } } } diff --git a/library/Icingadb/Widget/ItemList/CommandTransportList.php b/library/Icingadb/Widget/ItemList/CommandTransportList.php new file mode 100644 index 00000000..615e7d64 --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommandTransportList.php @@ -0,0 +1,22 @@ +getAttributes()->add('class', 'command-transport-list'); + $this->setDetailUrl(Url::fromPath('icingadb/command-transport/show')); + } + + protected function getItemClass() + { + return CommandTransportListItem::class; + } +} diff --git a/library/Icingadb/Widget/ItemList/CommandTransportListItem.php b/library/Icingadb/Widget/ItemList/CommandTransportListItem.php new file mode 100644 index 00000000..681f939e --- /dev/null +++ b/library/Icingadb/Widget/ItemList/CommandTransportListItem.php @@ -0,0 +1,69 @@ +setDetailFilter(Filter::equal('name', $this->item->name)); + } + + protected function assembleHeader(BaseHtmlElement $header) + { + } + + protected function assembleMain(BaseHtmlElement $main) + { + $main->add(new Link( + new HtmlElement('strong', null, $this->item->name), + Url::fromPath('icingadb/command-transport/show', ['name' => $this->item->name]) + )); + + $main->add(new Link( + new Icon('trash', ['title' => sprintf(t('Remove command transport "%s"'), $this->item->name)]), + Url::fromPath('icingadb/command-transport/remove', ['name' => $this->item->name]), + [ + 'class' => 'pull-right action-link', + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ] + )); + + if ($this->getOrder() + 1 < $this->list->count()) { + $main->add((new Link( + new Icon('arrow-down'), + Url::fromPath('icingadb/command-transport/sort', [ + 'name' => $this->item->name, + 'pos' => $this->getOrder() + 1 + ]), + ['class' => 'pull-right action-link'] + ))->setBaseTarget('_self')); + } + + if ($this->getOrder() > 0) { + $main->add((new Link( + new Icon('arrow-up'), + Url::fromPath('icingadb/command-transport/sort', [ + 'name' => $this->item->name, + 'pos' => $this->getOrder() - 1 + ]), + ['class' => 'pull-right action-link'] + ))->setBaseTarget('_self')); + } + } + + protected function createVisual() + { + } +} diff --git a/public/css/compat/spinner.less b/public/css/compat/spinner.less new file mode 100644 index 00000000..a55e81cd --- /dev/null +++ b/public/css/compat/spinner.less @@ -0,0 +1,19 @@ +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +a.spinner.active > i.fa-sync-alt, +button.spinner.active > i.fa-sync-alt, +i.fa-sync-alt.spinner.active { + &:before { + // We use a different icon font, hence why the unicode is redefined (overridden) here + content: '\f2f1'; + } +} + +a.spinner.active > i.fa-trash, +button.spinner.active > i.fa-trash, +i.fa-trash.spinner.active { + &:before { + // We use a different icon font, hence why the unicode is redefined (overridden) here + content: '\f1f8'; + } +} diff --git a/public/css/form/schedule-service-downtime-form.less b/public/css/form/schedule-service-downtime-form.less new file mode 100644 index 00000000..a65264d2 --- /dev/null +++ b/public/css/form/schedule-service-downtime-form.less @@ -0,0 +1,21 @@ +.downtime-duration { + > label { + display: flex; + flex: 1 1 auto; + flex-flow: row-reverse; + + input { + flex: 1 1 auto; + } + + span { + margin-left: .5em; + padding: .5625em 0; + line-height: 1.1em; + } + + &:not(:last-child) { + margin-right: 1.5em; + } + } +} diff --git a/public/js/action-list.js b/public/js/action-list.js index 26642138..9204a0c2 100644 --- a/public/js/action-list.js +++ b/public/js/action-list.js @@ -42,7 +42,7 @@ ActionList.prototype.onClick = function (event) { var _this = event.data.self; var $activeItems; - var $target = $(this); + var $target = $(event.currentTarget); var $item = $target.closest('.list-item'); var $list = $item.parent('.action-list'); @@ -90,7 +90,7 @@ $activeItems = $list.find('.list-item.active'); if ($activeItems.length === 0) { - if (_this.icinga.loader.getLinkTargetFor($item).attr('id') === 'col2') { + if (_this.icinga.loader.getLinkTargetFor($target).attr('id') === 'col2') { _this.icinga.ui.layout1col(); } } else { @@ -107,7 +107,7 @@ } _this.icinga.loader.loadUrl( - url, _this.icinga.loader.getLinkTargetFor($item) + url, _this.icinga.loader.getLinkTargetFor($target) ); } }; @@ -147,7 +147,7 @@ var $list = $target.find('.action-list'); - if ($list.length && $list.is('[data-icinga-multiselect-url]')) { + if ($list.length && $list.is('[data-icinga-multiselect-url], [data-icinga-detail-url]')) { var _this = event.data.self; var detailUrl = _this.icinga.utils.parseUrl(_this.icinga.history.getCol2State().replace(/^#!/, ''));