Merge branch 'master' into feature/user-and-group-management-8826

This commit is contained in:
Johannes Meyer 2015-06-02 10:44:13 +02:00
commit cd0c418854
38 changed files with 415 additions and 217 deletions

2
Vagrantfile vendored
View File

@ -22,7 +22,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provision :shell, :path => ".puppet/manifests/puppet.sh" config.vm.provision :shell, :path => ".puppet/manifests/puppet.sh"
config.vm.provider :virtualbox do |v, override| config.vm.provider :virtualbox do |v, override|
override.vm.box = "puppetlabs/centos-6.5-64-puppet" override.vm.box = "puppetlabs/centos-6.6-64-puppet"
v.customize ["modifyvm", :id, "--memory", "1024"] v.customize ["modifyvm", :id, "--memory", "1024"]
end end

View File

@ -84,7 +84,7 @@ class ConfigController extends Controller
public function indexAction() public function indexAction()
{ {
if ($this->firstAllowedAction === null) { if ($this->firstAllowedAction === null) {
throw new SecurityException('No permission for configuration'); throw new SecurityException($this->translate('No permission for configuration'));
} }
$action = $this->getTabs()->get($this->firstAllowedAction); $action = $this->getTabs()->get($this->firstAllowedAction);
if (substr($action->getUrl()->getPath(), 0, 7) === 'config/') { if (substr($action->getUrl()->getPath(), 0, 7) === 'config/') {
@ -271,7 +271,7 @@ class ConfigController extends Controller
$configForm->remove($authBackend); $configForm->remove($authBackend);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
Notification::error($e->getMessage()); Notification::error($e->getMessage());
return; return false;
} }
if ($configForm->save()) { if ($configForm->save()) {
@ -353,7 +353,7 @@ class ConfigController extends Controller
$configForm->remove($resource); $configForm->remove($resource);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
Notification::error($e->getMessage()); Notification::error($e->getMessage());
return; return false;
} }
if ($configForm->save()) { if ($configForm->save()) {

View File

@ -78,7 +78,7 @@ class DashboardController extends ActionController
$dashboard = $this->dashboard; $dashboard = $this->dashboard;
$form = new DashletForm(); $form = new DashletForm();
$form->setDashboard($dashboard); $form->setDashboard($dashboard);
$form->setSubmitLabel(t('Update Dashlet')); $form->setSubmitLabel($this->translate('Update Dashlet'));
if (! $this->_request->getParam('pane')) { if (! $this->_request->getParam('pane')) {
throw new Zend_Controller_Action_Exception( throw new Zend_Controller_Action_Exception(
'Missing parameter "pane"', 'Missing parameter "pane"',

View File

@ -37,7 +37,7 @@ class ErrorController extends ActionController
$path = array_shift($path); $path = array_shift($path);
$this->getResponse()->setHttpResponseCode(404); $this->getResponse()->setHttpResponseCode(404);
$this->view->message = $this->translate('Page not found.'); $this->view->message = $this->translate('Page not found.');
if ($modules->hasInstalled($path) && ! $modules->hasEnabled($path)) { if ($this->Auth()->isAuthenticated() && $modules->hasInstalled($path) && ! $modules->hasEnabled($path)) {
$this->view->message .= ' ' . sprintf( $this->view->message .= ' ' . sprintf(
$this->translate('Enabling the "%s" module might help!'), $this->translate('Enabling the "%s" module might help!'),
$path $path

View File

@ -1,7 +1,7 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use Icinga\Module\Monitoring\Controller; use Icinga\Web\Controller;
use Icinga\Web\Url; use Icinga\Web\Url;
use Icinga\Web\Widget\Tabextension\DashboardAction; use Icinga\Web\Widget\Tabextension\DashboardAction;
use Icinga\Web\Widget\Tabextension\OutputFormat; use Icinga\Web\Widget\Tabextension\OutputFormat;

View File

@ -149,7 +149,7 @@ class ResourceConfigForm extends ConfigForm
} }
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
Notification::error($e->getMessage()); Notification::error($e->getMessage());
return; return false;
} }
if ($this->save()) { if ($this->save()) {

15
doc/configuration.md Normal file
View File

@ -0,0 +1,15 @@
# <a id="configuration"></a> Configuration
## Overview
Apart from its web configuration capabilities, the local configuration is
stored in `/etc/icingaweb2` by default (depending on your config setup).
Location | File | Description
------------------------------|-----------------------|---------------------------
. | config.ini | General configuration (logging, preferences)
. | resources.ini | Global resources (Icinga Web 2 database for preferences and authentication, icinga ido database)
. | roles.ini | User specific roles (e.g. `administrators`) and permissions
. | [authentication.ini](authentication.md) | Authentication backends (e.g. database)
enabledModules | Symlink | Contains symlinks to enabled modules from `/usr/share/icingaweb2/modules/*`. Defaults to [monitoring](modules/monitoring/doc/configuration.md) and `doc`.
modules | Directory | Module specific configuration

View File

@ -71,7 +71,7 @@ class Manager
$user $user
); );
$preferences = new Preferences($preferencesStore->load()); $preferences = new Preferences($preferencesStore->load());
} catch (NotReadableError $e) { } catch (Exception $e) {
Logger::error( Logger::error(
new IcingaException( new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown: %s', 'Cannot load preferences for user "%s". An exception was thrown: %s',

View File

@ -62,7 +62,7 @@ class IniEditor
$this->commentIndentation = array_key_exists('commentIndentation', $options) $this->commentIndentation = array_key_exists('commentIndentation', $options)
? $options['commentIndentation'] : 43; ? $options['commentIndentation'] : 43;
$this->sectionSeparators = array_key_exists('sectionSeparators', $options) $this->sectionSeparators = array_key_exists('sectionSeparators', $options)
? $options['sectionSeparators'] : 2; ? $options['sectionSeparators'] : 1;
} }
/** /**
@ -314,7 +314,7 @@ class IniEditor
public function getText() public function getText()
{ {
$this->cleanUpWhitespaces(); $this->cleanUpWhitespaces();
return implode(PHP_EOL, $this->text); return rtrim(implode(PHP_EOL, $this->text)) . PHP_EOL;
} }
/** /**

View File

@ -216,25 +216,22 @@ class FilterEditor extends AbstractWidget
$filter = $this->getFilter(); $filter = $this->getFilter();
if ($search !== null) { if ($search !== null) {
if (empty($this->searchColumns)) { if (strpos($search, '=') !== false) {
if (strpos($search, '=') === false) {
Notification::error(mt('monitoring', 'Cannot search here'));
return $this;
} else {
list($k, $v) = preg_split('/=/', $search); list($k, $v) = preg_split('/=/', $search);
$filter = $this->mergeRootExpression($filter, trim($k), '=', ltrim($v)); $filter = $this->mergeRootExpression($filter, trim($k), '=', ltrim($v));
} } elseif (! empty($this->searchColumns)) {
} else { if (! $this->resetSearchColumns($filter)) {
if (false === $this->resetSearchColumns($filter)) {
$filter = Filter::matchAll(); $filter = Filter::matchAll();
} }
$filters = array(); $filters = array();
$search = ltrim($search); $search = ltrim($search);
foreach ($this->searchColumns as $searchColumn) { foreach ($this->searchColumns as $searchColumn) {
$filters[] = Filter::expression($searchColumn, '=', "*$search*"); $filters[] = Filter::expression($searchColumn, '=', "*$search*");
} }
$filter->andFilter(new FilterOr($filters)); $filter = $filter->andFilter(new FilterOr($filters));
} else {
Notification::error(mt('monitoring', 'Cannot search here'));
return $this;
} }
$url = $this->url()->setQueryString( $url = $this->url()->setQueryString(

View File

@ -56,6 +56,7 @@ class Monitoring_HostsController extends Controller
'host_icon_image', 'host_icon_image',
'host_icon_image_alt', 'host_icon_image_alt',
'host_name', 'host_name',
'host_address',
'host_state', 'host_state',
'host_problem', 'host_problem',
'host_handled', 'host_handled',
@ -94,6 +95,7 @@ class Monitoring_HostsController extends Controller
'host_icon_image', 'host_icon_image',
'host_icon_image_alt', 'host_icon_image_alt',
'host_name', 'host_name',
'host_address',
'host_state', 'host_state',
'host_problem', 'host_problem',
'host_handled', 'host_handled',
@ -131,14 +133,14 @@ class Monitoring_HostsController extends Controller
$this->view->objects = $this->hostList; $this->view->objects = $this->hostList;
$this->view->unhandledObjects = $this->hostList->getUnhandledObjects(); $this->view->unhandledObjects = $this->hostList->getUnhandledObjects();
$this->view->problemObjects = $this->hostList->getProblemObjects(); $this->view->problemObjects = $this->hostList->getProblemObjects();
$this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/hosts/acknowledge-problem')
->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter());
$this->view->downtimeUnhandledLink = Url::fromPath('monitoring/hosts/schedule-downtime') $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/hosts/schedule-downtime')
->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()); ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeLink = Url::fromPath('monitoring/hosts/schedule-downtime') $this->view->downtimeLink = Url::fromPath('monitoring/hosts/schedule-downtime')
->setQueryString($this->hostList->getProblemObjects()->objectsFilter()); ->setQueryString($this->hostList->getProblemObjects()->objectsFilter()->toQueryString());
$this->view->acknowledgedObjects = $this->hostList->getAcknowledgedObjects(); $this->view->acknowledgedObjects = $this->hostList->getAcknowledgedObjects();
$this->view->acknowledgeLink = Url::fromPath('monitoring/hosts/acknowledge-problem')
->setQueryString($this->hostList->getUnacknowledgedObjects()->objectsFilter()->toQueryString());
$this->view->unacknowledgedObjects = $this->hostList->getUnacknowledgedObjects();
$this->view->objectsInDowntime = $this->hostList->getObjectsInDowntime(); $this->view->objectsInDowntime = $this->hostList->getObjectsInDowntime();
$this->view->inDowntimeLink = Url::fromPath('monitoring/list/hosts') $this->view->inDowntimeLink = Url::fromPath('monitoring/list/hosts')
->setQueryString( ->setQueryString(

View File

@ -53,6 +53,7 @@ class Monitoring_ServicesController extends Controller
'host_icon_image', 'host_icon_image',
'host_icon_image_alt', 'host_icon_image_alt',
'host_name', 'host_name',
'host_address',
'host_output', 'host_output',
'host_state', 'host_state',
'host_problem', 'host_problem',
@ -100,6 +101,7 @@ class Monitoring_ServicesController extends Controller
'host_icon_image', 'host_icon_image',
'host_icon_image_alt', 'host_icon_image_alt',
'host_name', 'host_name',
'host_address',
'host_output', 'host_output',
'host_state', 'host_state',
'host_problem', 'host_problem',
@ -147,13 +149,14 @@ class Monitoring_ServicesController extends Controller
$this->view->objects = $this->serviceList; $this->view->objects = $this->serviceList;
$this->view->unhandledObjects = $this->serviceList->getUnhandledObjects(); $this->view->unhandledObjects = $this->serviceList->getUnhandledObjects();
$this->view->problemObjects = $this->serviceList->getProblemObjects(); $this->view->problemObjects = $this->serviceList->getProblemObjects();
$this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/services/acknowledge-problem')
->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeUnhandledLink = Url::fromPath('monitoring/services/schedule-downtime') $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/services/schedule-downtime')
->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString()); ->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeLink = Url::fromPath('monitoring/services/schedule-downtime') $this->view->downtimeLink = Url::fromPath('monitoring/services/schedule-downtime')
->setQueryString($this->serviceList->getProblemObjects()->objectsFilter()->toQueryString()); ->setQueryString($this->serviceList->getProblemObjects()->objectsFilter()->toQueryString());
$this->view->acknowledgedObjects = $acknowledgedObjects; $this->view->acknowledgedObjects = $acknowledgedObjects;
$this->view->acknowledgeLink = Url::fromPath('monitoring/services/acknowledge-problem')
->setQueryString($this->serviceList->getUnacknowledgedObjects()->objectsFilter()->toQueryString());
$this->view->unacknowledgedObjects = $this->serviceList->getUnacknowledgedObjects();
$this->view->objectsInDowntime = $this->serviceList->getObjectsInDowntime(); $this->view->objectsInDowntime = $this->serviceList->getObjectsInDowntime();
$this->view->inDowntimeLink = Url::fromPath('monitoring/list/services') $this->view->inDowntimeLink = Url::fromPath('monitoring/list/services')
->setQueryString($this->serviceList->getObjectsInDowntime() ->setQueryString($this->serviceList->getObjectsInDowntime()

View File

@ -1,6 +1,8 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use Icinga\Module\Monitoring\Object\Macro;
/** /**
* Generate icons to describe a given hosts state * Generate icons to describe a given hosts state
*/ */
@ -26,7 +28,7 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
{ {
if ($object->host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) { if ($object->host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) {
return $this->view->img( return $this->view->img(
'img/icons/' . $this->view->resolveMacros($object->host_icon_image, $object), 'img/icons/' . Macro::resolveMacros($object->host_icon_image, $object),
null, null,
array( array(
'alt' => $object->host_icon_image_alt, 'alt' => $object->host_icon_image_alt,
@ -48,7 +50,7 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
{ {
if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) { if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) {
return $this->view->img( return $this->view->img(
'img/icons/' . $this->view->resolveMacros($object->service_icon_image, $object), 'img/icons/' . Macro::resolveMacros($object->service_icon_image, $object),
null, null,
array( array(
'alt' => $object->service_icon_image_alt, 'alt' => $object->service_icon_image_alt,

View File

@ -11,6 +11,7 @@
<table class="avp newsection"> <table class="avp newsection">
<tbody> <tbody>
<?= $this->render('show/components/notes.phtml') ?>
<?= $this->render('show/components/acknowledgement.phtml') ?> <?= $this->render('show/components/acknowledgement.phtml') ?>
<?= $this->render('show/components/comments.phtml') ?> <?= $this->render('show/components/comments.phtml') ?>
<?= $this->render('show/components/notifications.phtml') ?> <?= $this->render('show/components/notifications.phtml') ?>

View File

@ -72,9 +72,10 @@
<?php <?php
$unhandledCount = count($unhandledObjects); $unhandledCount = count($unhandledObjects);
$problemCount = count($problemObjects); $problemCount = count($problemObjects);
$unackCount = count($unacknowledgedObjects);
?> ?>
<?php if ($problemCount || $unhandledCount): ?> <?php if ($problemCount || $unhandledCount || $unackCount): ?>
<h3> <h3>
<?= $this->icon('attention-alt') ?> <?= $this->icon('attention-alt') ?>
<?= $this->translatePlural( <?= $this->translatePlural(
@ -110,6 +111,24 @@
); ?> ); ?>
<?php endif; ?> <?php endif; ?>
<?php
if ($unackCount > 0): ?>
<br>
<?= $this->qlink(
sprintf(
$this->translatePlural(
'Acknowledge %u unacknowledged problem hosts',
'Acknowledge %u unacknowledged problem hosts',
$unackCount
),
$unackCount
),
$acknowledgeLink,
null,
array('icon' => 'ok')
); ?>
<?php endif; ?>
<?php if ($unhandledCount): ?> <?php if ($unhandledCount): ?>
<p> <p>
<?= sprintf( <?= sprintf(
@ -136,21 +155,6 @@
array('icon' => 'plug') array('icon' => 'plug')
); ?> ); ?>
<br>
<?= $this->qlink(
sprintf(
$this->translatePlural(
'Acknowledge %u unhandled problem host',
'Acknowledge %u unhandled problem hosts',
$unhandledCount
),
$unhandledCount
),
$acknowledgeUnhandledLink,
null,
array('icon' => 'ok')
); ?>
<?php endif; ?> <?php endif; ?>
<?php endif;?> <?php endif;?>

View File

@ -15,7 +15,15 @@ $this->baseFilter = isset($this->baseFilter) ? $this->baseFilter : null;
$selfUrl = 'monitoring/list/hosts'; $selfUrl = 'monitoring/list/hosts';
$currentUrl = Url::fromRequest()->getRelativeUrl(); $currentUrl = Url::fromRequest()->getRelativeUrl();
?><div class="tinystatesummary" <?= $this->compact ? ' data-base-target="col1"' : ''; ?>> ?><div class="tinystatesummary" <?= $this->compact ? ' data-base-target="col1"' : ''; ?>>
<?= sprintf($this->translate('%s hosts:'), $this->stats->hosts_total); ?> <?= $this->qlink(
sprintf($this->translatePlural('%u Host', '%u Hosts', $this->stats->hosts_total), $this->stats->hosts_total),
$selfUrl,
null,
array('title' => sprintf(
$this->translatePlural('List %u host', 'List all %u hosts', $this->stats->hosts_total),
$this->stats->hosts_total
))
) ?>&#58;
<span class="badges"> <span class="badges">
<?php if ($this->stats->hosts_up): ?> <?php if ($this->stats->hosts_up): ?>
<span class="state ok<?= $currentUrl === Url::fromPath($selfUrl, array('host_state' => 0))->getRelativeUrl() ? ' active' : ''; ?>"> <span class="state ok<?= $currentUrl === Url::fromPath($selfUrl, array('host_state' => 0))->getRelativeUrl() ? ' active' : ''; ?>">

View File

@ -17,7 +17,18 @@ $this->baseFilter = isset($this->baseFilter) ? $this->baseFilter : null;
$selfUrl = 'monitoring/list/services'; $selfUrl = 'monitoring/list/services';
$currentUrl = Url::fromRequest()->getRelativeUrl(); $currentUrl = Url::fromRequest()->getRelativeUrl();
?><div class="tinystatesummary" <?= $this->compact ? ' data-base-target="col1"' : ''; ?>> ?><div class="tinystatesummary" <?= $this->compact ? ' data-base-target="col1"' : ''; ?>>
<?= sprintf($this->translate('%s services:'), $this->stats->services_total); ?> <?= $this->qlink(
sprintf($this->translatePlural(
'%u Service', '%u Services', $this->stats->services_total),
$this->stats->services_total
),
$selfUrl,
null,
array('title' => sprintf(
$this->translatePlural('List %u service', 'List all %u services', $this->stats->services_total),
$this->stats->services_total
))
) ?>&#58;
<span class="badges"> <span class="badges">
<?php if ($this->stats->services_ok): ?> <?php if ($this->stats->services_ok): ?>
<span class="state ok<?= $currentUrl === Url::fromPath($selfUrl, array('service_state' => 0))->getRelativeUrl() ? ' active' : ''; ?>"> <span class="state ok<?= $currentUrl === Url::fromPath($selfUrl, array('service_state' => 0))->getRelativeUrl() ? ' active' : ''; ?>">

View File

@ -11,6 +11,7 @@
<table class="avp newsection"> <table class="avp newsection">
<tbody> <tbody>
<?= $this->render('show/components/notes.phtml') ?>
<?= $this->render('show/components/acknowledgement.phtml') ?> <?= $this->render('show/components/acknowledgement.phtml') ?>
<?= $this->render('show/components/comments.phtml') ?> <?= $this->render('show/components/comments.phtml') ?>
<?= $this->render('show/components/notifications.phtml') ?> <?= $this->render('show/components/notifications.phtml') ?>

View File

@ -70,9 +70,10 @@
<?php <?php
$unhandledCount = count($unhandledObjects); $unhandledCount = count($unhandledObjects);
$problemCount = count($problemObjects); $problemCount = count($problemObjects);
$unackCount = count($unacknowledgedObjects);
?> ?>
<?php if ($problemCount || $unhandledCount): ?> <?php if ($problemCount || $unhandledCount || $unackCount): ?>
<div> <div>
<h3> <h3>
<?= $this->icon('attention-alt') ?> <?= $this->icon('attention-alt') ?>
@ -105,6 +106,23 @@
); ?> ); ?>
<?php endif ?> <?php endif ?>
<?php if ($unackCount > 0): ?>
<br>
<?= $this->qlink(
sprintf(
$this->translatePlural(
'Acknowledge %u unacknowledged problem service',
'Acknowledge %u unacknowledged problem services',
$unackCount
),
$unackCount
),
$acknowledgeLink,
null,
array('icon' => 'ok')
); ?>
<?php endif; ?>
<?php if ($unhandledCount): ?> <?php if ($unhandledCount): ?>
<p> <p>
<?= sprintf($this->translate('There are %s unhandled problem services. ' . <?= sprintf($this->translate('There are %s unhandled problem services. ' .
@ -126,20 +144,6 @@
array('icon' => 'plug') array('icon' => 'plug')
); ?> ); ?>
<br>
<?= $this->qlink(
sprintf(
$this->translatePlural(
'Acknowledge %u unhandled problem service',
'Acknowledge %u unhandled problem services',
$unhandledCount
),
$unhandledCount
),
$acknowledgeUnhandledLink,
null,
array('icon' => 'ok')
); ?>
<?php endif ?> <?php endif ?>
</div> </div>

View File

@ -1,41 +1,16 @@
<?php <?php
$links = array();
// add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201 // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
$newTabInfo = sprintf('<span class="info-box display-on-hover"> %s </span>', $this->translate('opens in new window')); $newTabInfo = sprintf('<span class="info-box display-on-hover"> %s </span>', $this->translate('opens in new window'));
$linkText = '<a href="%s" target="_blank">%s ' . $newTabInfo . '</a>'; $links = $object->getActionUrls();
$localLinkText = '<a href="%s">%s</a>'; foreach ($links as $i => $link) {
$links[$i] = sprintf('<a href="%s" target="_blank">%s ' . $newTabInfo . '</a>', $link, 'Action');
if ($object->notes_url) {
if (strpos($object->notes_url, "' ") === false) {
$links[] = sprintf($linkText, $this->resolveMacros($object->notes_url, $object), 'Notes');
} else {
// TODO: We should find out document what's going on here. Looks strange :p
foreach(explode("' ", $object->notes_url) as $url) {
$url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
$url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
$links[] = sprintf($linkText, $this->resolveMacros($url, $object), 'Notes');
}
}
}
if ($object->action_url) {
if (strpos($object->action_url, "' ") === false) {
$links[] = sprintf($linkText, $this->resolveMacros($object->action_url, $object), 'Action');
} else {
// TODO: We should find out document what's going on here. Looks strange :p
foreach(explode("' ", $object->action_url) as $url) {
$url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
$url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
$links[] = sprintf($linkText, $this->resolveMacros($url, $object), 'Action');
}
}
} }
if (isset($this->actions)) { if (isset($this->actions)) {
foreach ($this->actions as $id => $action) { foreach ($this->actions as $id => $action) {
$links[] = sprintf($localLinkText, $action, $id); $links[] = sprintf('<a href="%s">%s</a>', $action, $id);
} }
} }
@ -46,5 +21,5 @@ if (empty($links)) {
?> ?>
<tr> <tr>
<th><?= $this->translate('Actions') ?></th> <th><?= $this->translate('Actions') ?></th>
<td><?= implode("\n ", $links) . "\n" ?></td> <td><?= implode("<br>", $links) ?></td>
</tr> </tr>

View File

@ -0,0 +1,26 @@
<?php
$notes = trim($object->getNotes());
$links = $object->getNotesUrls();
if (! empty($links) || ! empty($notes)): ?>
<tr>
<th><?= $this->translate('Notes') ?></th>
<td>
<?php
if (! empty($notes)) {
echo $notes . '<br>';
}
// add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
$newTabInfo = sprintf(
'<span class="info-box display-on-hover"> %s </span>',
$this->translate('opens in new window')
);
$linkText = '<a href="%s" target="_blank">%s ' . $newTabInfo . '</a>';
foreach ($links as $i => $link) {
$links[$i] = sprintf($linkText, $this->escape($link), $this->escape($link));
}
echo implode('<br>', $links);
?>
</td>
</tr>
<?php endif ?>

View File

@ -0,0 +1,16 @@
# <a id="monitoring-configuration"></a> Monitoring Module Configuration
## Overview
Apart from its web configuration capabilities, the local configuration is
stored in `/etc/icingaweb2` by default (depending on your config setup).
Location | File | Description
------------------------------|-----------------------|---------------------------
modules/monitoring | Directory | `monitoring` module specific configuration
modules/monitoring | config.ini | Security settings (e.g. protected custom vars) for the `monitoring` module
modules/monitoring | backends.ini | Backend type and resources (e.g. Icinga IDO DB)
modules/monitoring | [instances.ini](instances.md#instances) | Instances and their transport (e.g. local external command pipe)

View File

@ -43,6 +43,7 @@ class StatusQuery extends IdoQuery
'host_icon_image' => 'h.icon_image', 'host_icon_image' => 'h.icon_image',
'host_icon_image_alt' => 'h.icon_image_alt', 'host_icon_image_alt' => 'h.icon_image_alt',
'host_action_url' => 'h.action_url', 'host_action_url' => 'h.action_url',
'host_notes' => 'h.notes',
'host_notes_url' => 'h.notes_url' 'host_notes_url' => 'h.notes_url'
), ),
'hoststatus' => array( 'hoststatus' => array(
@ -179,6 +180,7 @@ class StatusQuery extends IdoQuery
'service_icon_image' => 's.icon_image', 'service_icon_image' => 's.icon_image',
'service_icon_image_alt' => 's.icon_image_alt', 'service_icon_image_alt' => 's.icon_image_alt',
'service_action_url' => 's.action_url', 'service_action_url' => 's.action_url',
'service_notes' => 's.notes',
'service_notes_url' => 's.notes_url', 'service_notes_url' => 's.notes_url',
'object_type' => '(\'service\')' 'object_type' => '(\'service\')'
), ),

View File

@ -4,6 +4,7 @@
namespace Icinga\Module\Monitoring\Command\Transport; namespace Icinga\Module\Monitoring\Command\Transport;
use Exception; use Exception;
use RuntimeException;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Module\Monitoring\Command\Exception\TransportException; use Icinga\Module\Monitoring\Command\Exception\TransportException;
@ -123,11 +124,15 @@ class LocalCommandFile implements CommandTransportInterface
$file->fwrite($commandString . "\n"); $file->fwrite($commandString . "\n");
$file->fflush(); $file->fflush();
} catch (Exception $e) { } catch (Exception $e) {
$message = $e->getMessage();
if ($e instanceof RuntimeException && ($pos = strrpos($message, ':')) !== false) {
// Assume RuntimeException thrown by SplFileObject in the format: __METHOD__ . "({$filename}): Message"
$message = substr($message, $pos + 1);
}
throw new TransportException( throw new TransportException(
'Can\'t send external Icinga command "%s" to the local command file "%s": %s', 'Can\'t send external Icinga command to the local command file "%s": %s',
$commandString,
$this->path, $this->path,
$e $message
); );
} }
} }

View File

@ -201,8 +201,7 @@ class RemoteCommandFile implements CommandTransportInterface
exec($ssh, $output, $status); exec($ssh, $output, $status);
if ($status !== 0) { if ($status !== 0) {
throw new TransportException( throw new TransportException(
'Can\'t send external Icinga command "%s": %s', 'Can\'t send external Icinga command: %s',
$ssh,
implode(' ', $output) implode(' ', $output)
); );
} }

View File

@ -7,7 +7,7 @@ use InvalidArgumentException;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/** /**
* A Icinga host * An Icinga host
*/ */
class Host extends MonitoredObject class Host extends MonitoredObject
{ {
@ -119,6 +119,7 @@ class Host extends MonitoredObject
'host_max_check_attempts', 'host_max_check_attempts',
'host_name', 'host_name',
'host_next_check', 'host_next_check',
'host_notes',
'host_notes_url', 'host_notes_url',
'host_notifications_enabled', 'host_notifications_enabled',
'host_notifications_enabled_changed', 'host_notifications_enabled_changed',
@ -188,4 +189,16 @@ class Host extends MonitoredObject
} }
return $text; return $text;
} }
public function getNotesUrls()
{
return $this->resolveAllStrings(
MonitoredObject::parseAttributeUrls($this->host_notes_url)
);
}
public function getNotes()
{
return $this->host_notes;
}
} }

View File

@ -99,4 +99,19 @@ class HostList extends ObjectList
->applyFilter(clone $this->filter) ->applyFilter(clone $this->filter)
->where('downtime_objecttype', 'host'); ->where('downtime_objecttype', 'host');
} }
/**
* @return ObjectList
*/
public function getUnacknowledgedObjects()
{
$unhandledObjects = array();
foreach ($this as $object) {
if (! in_array((int) $object->state, array(0, 99)) &&
(bool) $object->host_acknowledged === false) {
$unhandledObjects[] = $object;
}
}
return $this->newFromArray($unhandledObjects);
}
} }

View File

@ -1,20 +1,25 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Zend_View_Helper_Abstract; namespace Icinga\Module\Monitoring\Object;
use Icinga\Module\Monitoring\Object\MonitoredObject;
class Zend_View_Helper_ResolveMacros extends Zend_View_Helper_Abstract /**
* Expand macros in string in the context of MonitoredObjects
*/
class Macro
{ {
/** /**
* Known icinga macros * Known icinga macros
* *
* @var array * @var array
*/ */
private $icingaMacros = array( private static $icingaMacros = array(
'HOSTNAME' => 'host_name', 'HOSTNAME' => 'host_name',
'HOSTADDRESS' => 'host_address', 'HOSTADDRESS' => 'host_address',
'SERVICEDESC' => 'service_description' 'SERVICEDESC' => 'service_description',
'host.name' => 'host_name',
'host.address' => 'host_address',
'service.description' => 'service_description'
); );
/** /**
@ -25,12 +30,12 @@ class Zend_View_Helper_ResolveMacros extends Zend_View_Helper_Abstract
* *
* @return string The substituted or unchanged string * @return string The substituted or unchanged string
*/ */
public function resolveMacros($input, $object) public static function resolveMacros($input, $object)
{ {
$matches = array(); $matches = array();
if (preg_match_all('@\$([^\$\s]+)\$@', $input, $matches)) { if (preg_match_all('@\$([^\$\s]+)\$@', $input, $matches)) {
foreach ($matches[1] as $key => $value) { foreach ($matches[1] as $key => $value) {
$newValue = $this->resolveMacro($value, $object); $newValue = self::resolveMacro($value, $object);
if ($newValue !== $value) { if ($newValue !== $value) {
$input = str_replace($matches[0][$key], $newValue, $input); $input = str_replace($matches[0][$key], $newValue, $input);
} }
@ -48,10 +53,10 @@ class Zend_View_Helper_ResolveMacros extends Zend_View_Helper_Abstract
* *
* @return string The new value or the macro if it cannot be resolved * @return string The new value or the macro if it cannot be resolved
*/ */
public function resolveMacro($macro, $object) public static function resolveMacro($macro, $object)
{ {
if (array_key_exists($macro, $this->icingaMacros) && $object->{$this->icingaMacros[$macro]} !== false) { if (array_key_exists($macro, self::$icingaMacros) && $object->{self::$icingaMacros[$macro]} !== false) {
return $object->{$this->icingaMacros[$macro]}; return $object->{self::$icingaMacros[$macro]};
} }
if (array_key_exists($macro, $object->customvars)) { if (array_key_exists($macro, $object->customvars)) {
return $object->customvars[$macro]; return $object->customvars[$macro];

View File

@ -563,4 +563,73 @@ abstract class MonitoredObject implements Filterable
} }
return null; return null;
} }
/**
* The notes for this monitored object
*
* @return string The notes as a string
*/
public abstract function getNotes();
/**
* Get all note urls configured for this monitored object
*
* @return array All note urls as a string
*/
public abstract function getNotesUrls();
/**
* Get all action urls configured for this monitored object
*
* @return array All note urls as a string
*/
public function getActionUrls()
{
return $this->resolveAllStrings(
MonitoredObject::parseAttributeUrls($this->action_url)
);
}
/**
* Resolve macros in all given strings in the current object context
*
* @param array $strs An array of urls as string
* @return type
*/
protected function resolveAllStrings(array $strs)
{
foreach ($strs as $i => $str) {
$strs[$i] = Macro::resolveMacros($str, $this);
}
return $strs;
}
/**
* Parse the content of the action_url or notes_url attributes
*
* Find all occurences of http links, separated by whitespaces and quoted
* by single or double-ticks.
*
* @link http://docs.icinga.org/latest/de/objectdefinitions.html
*
* @param string $urlString A string containing one or more urls
* @return array Array of urls as strings
*/
public static function parseAttributeUrls($urlString)
{
if (empty($urlString)) {
return array();
}
if (strpos($urlString, "' ") === false) {
$links[] = $urlString;
} else {
// parse notes-url format
foreach (explode("' ", $urlString) as $url) {
$url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
$url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
$links[] = $url;
}
}
return $links;
}
} }

View File

@ -193,6 +193,11 @@ abstract class ObjectList implements Countable, IteratorAggregate
return $this->newFromArray($handledObjects); return $this->newFromArray($handledObjects);
} }
/**
* @return ObjectList
*/
public abstract function getUnacknowledgedObjects();
/** /**
* Create a ObjectList from an array of hosts without querying a backend * Create a ObjectList from an array of hosts without querying a backend
* *

View File

@ -7,7 +7,7 @@ use InvalidArgumentException;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/** /**
* A Icinga service * An Icinga service
*/ */
class Service extends MonitoredObject class Service extends MonitoredObject
{ {
@ -147,6 +147,7 @@ class Service extends MonitoredObject
'service_last_state_change', 'service_last_state_change',
'service_long_output', 'service_long_output',
'service_next_check', 'service_next_check',
'service_notes',
'service_notes_url', 'service_notes_url',
'service_notifications_enabled', 'service_notifications_enabled',
'service_notifications_enabled_changed', 'service_notifications_enabled_changed',
@ -198,4 +199,16 @@ class Service extends MonitoredObject
} }
return $text; return $text;
} }
public function getNotesUrls()
{
return $this->resolveAllStrings(
MonitoredObject::parseAttributeUrls($this->service_notes_url)
);
}
public function getNotes()
{
return $this->service_notes;
}
} }

View File

@ -147,4 +147,19 @@ class ServiceList extends ObjectList
->applyFilter(clone $this->filter) ->applyFilter(clone $this->filter)
->where('downtime_objecttype', 'service'); ->where('downtime_objecttype', 'service');
} }
/**
* @return ObjectList
*/
public function getUnacknowledgedObjects()
{
$unhandledObjects = array();
foreach ($this as $object) {
if (! in_array((int) $object->state, array(0, 99)) &&
(bool) $object->service_acknowledged === false) {
$unhandledObjects[] = $object;
}
}
return $this->newFromArray($unhandledObjects);
}
} }

View File

@ -1,78 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\Modules\Monitoring\Application\Views\Helpers;
use Mockery;
use Zend_View_Helper_ResolveMacros;
use Icinga\Test\BaseTestCase;
require_once realpath(BaseTestCase::$moduleDir . '/monitoring/application/views/helpers/ResolveMacros.php');
class ResolveMacrosTest extends BaseTestCase
{
public function testHostMacros()
{
$hostMock = Mockery::mock('host');
$hostMock->host_name = 'test';
$hostMock->host_address = '1.1.1.1';
$helper = new Zend_View_Helper_ResolveMacros();
$this->assertEquals($helper->resolveMacros('$HOSTNAME$', $hostMock), $hostMock->host_name);
$this->assertEquals($helper->resolveMacros('$HOSTADDRESS$', $hostMock), $hostMock->host_address);
}
public function testServiceMacros()
{
$svcMock = Mockery::mock('service');
$svcMock->host_name = 'test';
$svcMock->host_address = '1.1.1.1';
$svcMock->service_description = 'a service';
$helper = new Zend_View_Helper_ResolveMacros();
$this->assertEquals($helper->resolveMacros('$HOSTNAME$', $svcMock), $svcMock->host_name);
$this->assertEquals($helper->resolveMacros('$HOSTADDRESS$', $svcMock), $svcMock->host_address);
$this->assertEquals($helper->resolveMacros('$SERVICEDESC$', $svcMock), $svcMock->service_description);
}
public function testCustomvars()
{
$objectMock = Mockery::mock('object');
$objectMock->customvars = array(
'CUSTOMVAR' => 'test'
);
$helper = new Zend_View_Helper_ResolveMacros();
$this->assertEquals($helper->resolveMacros('$CUSTOMVAR$', $objectMock), $objectMock->customvars['CUSTOMVAR']);
}
public function testFaultyMacros()
{
$hostMock = Mockery::mock('host');
$hostMock->host_name = 'test';
$hostMock->customvars = array(
'HOST' => 'te',
'NAME' => 'st'
);
$helper = new Zend_View_Helper_ResolveMacros();
$this->assertEquals(
$helper->resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $HOST$NAME$', $hostMock),
'$test $ HOSTNAME$ teNAME$'
);
}
public function testMacrosWithSpecialCharacters()
{
$objectMock = Mockery::mock('object');
$objectMock->customvars = array(
'V€RY_SP3C|@L' => 'not too special!'
);
$helper = new Zend_View_Helper_ResolveMacros();
$this->assertEquals(
$helper->resolveMacros('$V€RY_SP3C|@L$', $objectMock),
$objectMock->customvars['V€RY_SP3C|@L']
);
}
}

View File

@ -0,0 +1,78 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\Modules\Monitoring\Application\Views\Helpers;
use Mockery;
use Icinga\Test\BaseTestCase;
use Icinga\Module\Monitoring\Object\Macro;
require_once realpath(BaseTestCase::$moduleDir . '/monitoring/library/Monitoring/Object/Macro.php');
class MacroTest extends BaseTestCase
{
public function testHostMacros()
{
$hostMock = Mockery::mock('host');
$hostMock->host_name = 'test';
$hostMock->host_address = '1.1.1.1';
$this->assertEquals(Macro::resolveMacros('$HOSTNAME$', $hostMock), $hostMock->host_name);
$this->assertEquals(Macro::resolveMacros('$HOSTADDRESS$', $hostMock), $hostMock->host_address);
$this->assertEquals(Macro::resolveMacros('$host.name$', $hostMock), $hostMock->host_name);
$this->assertEquals(Macro::resolveMacros('$host.address$', $hostMock), $hostMock->host_address);
}
public function testServiceMacros()
{
$svcMock = Mockery::mock('service');
$svcMock->host_name = 'test';
$svcMock->host_address = '1.1.1.1';
$svcMock->service_description = 'a service';
$this->assertEquals(Macro::resolveMacros('$HOSTNAME$', $svcMock), $svcMock->host_name);
$this->assertEquals(Macro::resolveMacros('$HOSTADDRESS$', $svcMock), $svcMock->host_address);
$this->assertEquals(Macro::resolveMacros('$SERVICEDESC$', $svcMock), $svcMock->service_description);
$this->assertEquals(Macro::resolveMacros('$host.name$', $svcMock), $svcMock->host_name);
$this->assertEquals(Macro::resolveMacros('$host.address$', $svcMock), $svcMock->host_address);
$this->assertEquals(Macro::resolveMacros('$service.description$', $svcMock), $svcMock->service_description);
}
public function testCustomvars()
{
$objectMock = Mockery::mock('object');
$objectMock->customvars = array(
'CUSTOMVAR' => 'test'
);
$this->assertEquals(Macro::resolveMacros('$CUSTOMVAR$', $objectMock), $objectMock->customvars['CUSTOMVAR']);
}
public function testFaultyMacros()
{
$hostMock = Mockery::mock('host');
$hostMock->host_name = 'test';
$hostMock->customvars = array(
'HOST' => 'te',
'NAME' => 'st'
);
$this->assertEquals(
Macro::resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $HOST$NAME$', $hostMock),
'$test $ HOSTNAME$ teNAME$'
);
}
public function testMacrosWithSpecialCharacters()
{
$objectMock = Mockery::mock('object');
$objectMock->customvars = array(
'V€RY_SP3C|@L' => 'not too special!'
);
$this->assertEquals(
Macro::resolveMacros('$V€RY_SP3C|@L$', $objectMock),
$objectMock->customvars['V€RY_SP3C|@L']
);
}
}

View File

@ -96,11 +96,6 @@ select::-moz-focus-inner {
outline: 0; outline: 0;
} }
input:disabled, select:disabled {
background-color: #fff;
border-color: white;
}
form.inline { form.inline {
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@ -191,7 +191,6 @@ tr[href]:hover {
} }
tr.state[href]:hover td.state { tr.state[href]:hover td.state {
color: white;
background-color: #eee; background-color: #eee;
} }

View File

@ -274,6 +274,10 @@
} }
} }
// Disable all form controls to prevent resubmission except for our search input
// Note that disabled form inputs will not be enabled via JavaScript again
$form.find(':input:not(#search):not(:disabled)').prop('disabled', true);
icinga.loader.loadUrl(url, $target, data, method); icinga.loader.loadUrl(url, $target, data, method);
return false; return false;

View File

@ -521,12 +521,10 @@ EOD
key1 = "1" key1 = "1"
key2 = "2" key2 = "2"
[two] [two]
a.b = "c" a.b = "c"
d.e = "f" d.e = "f"
[three] [three]
key = "value" key = "value"
foo.bar = "raboof" foo.bar = "raboof"
@ -537,12 +535,10 @@ EOD;
key = "value" key = "value"
foo.bar = "raboof" foo.bar = "raboof"
[two] [two]
a.b = "c" a.b = "c"
d.e = "f" d.e = "f"
[one] [one]
key1 = "1" key1 = "1"
key2 = "2" key2 = "2"
@ -589,7 +585,6 @@ EOD;
; comment 1 ; comment 1
[one] [one]
; comment 2 ; comment 2
[two] [two]
EOD; EOD;
@ -598,7 +593,6 @@ EOD;
; comment 2 ; comment 2
[two] [two]
; comment 1 ; comment 1
[one] [one]
EOD; EOD;
@ -637,8 +631,8 @@ EOD;
); );
$this->assertEquals( $this->assertEquals(
$config, trim($config),
$writer->render(), trim($writer->render()),
'IniWriter does not preserve comments on empty lines' 'IniWriter does not preserve comments on empty lines'
); );
} }
@ -667,8 +661,8 @@ EOD;
); );
$this->assertEquals( $this->assertEquals(
$config, trim($config),
$writer->render(), trim($writer->render()),
'IniWriter does not preserve comments on property lines' 'IniWriter does not preserve comments on property lines'
); );
} }
@ -686,8 +680,8 @@ EOD;
); );
$this->assertEquals( $this->assertEquals(
$config, trim($config),
$writer->render(), trim($writer->render()),
'IniWriter does not preserve comments on empty section lines' 'IniWriter does not preserve comments on empty section lines'
); );
} }
@ -719,8 +713,8 @@ EOD;
); );
$this->assertEquals( $this->assertEquals(
$config, trim($config),
$writer->render(), trim($writer->render()),
'IniWriter does not preserve comments on property lines' 'IniWriter does not preserve comments on property lines'
); );
} }