summaryrefslogtreecommitdiffstats
path: root/library
diff options
context:
space:
mode:
Diffstat (limited to 'library')
-rw-r--r--library/Eventdb/Data/LegacyFilterParser.php153
-rw-r--r--library/Eventdb/Event.php120
-rw-r--r--library/Eventdb/Eventdb.php184
-rw-r--r--library/Eventdb/EventdbController.php181
-rw-r--r--library/Eventdb/Hook/DetailviewExtensionHook.php124
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php81
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php182
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/HostActions.php15
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php15
-rw-r--r--library/Eventdb/Test/BaseTestCase.php27
-rw-r--r--library/Eventdb/Test/Bootstrap.php35
-rw-r--r--library/Eventdb/Test/PseudoHost.php15
-rw-r--r--library/Eventdb/Test/PseudoMonitoringBackend.php14
-rw-r--r--library/Eventdb/Test/PseudoService.php16
-rw-r--r--library/Eventdb/Web/EventdbOutputFormat.php66
15 files changed, 1228 insertions, 0 deletions
diff --git a/library/Eventdb/Data/LegacyFilterParser.php b/library/Eventdb/Data/LegacyFilterParser.php
new file mode 100644
index 0000000..2907202
--- /dev/null
+++ b/library/Eventdb/Data/LegacyFilterParser.php
@@ -0,0 +1,153 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Exception\InvalidPropertyException;
+
+/**
+ * Class LegacyFilterParser
+ *
+ * Utility class to parse Icinga-web 1.x JSON filters of the EventDB module
+ */
+class LegacyFilterParser
+{
+ /**
+ * @param $json string JSON data
+ * @param $host string Icinga host name (for default host filter and logging)
+ * @param $service string Icinga service name (for logging)
+ *
+ * @return Filter
+ * @throws InvalidPropertyException When filter could not be parsed
+ */
+ static public function parse($json, $host, $service = null)
+ {
+ $json = static::fixJSONQuotes($json);
+
+ $data = json_decode($json, false, 5);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new InvalidPropertyException(
+ 'Could not decode legacy filter (host=%s)%s (%s): %s',
+ $host,
+ ($service ? ' (service=' . $service . ')' : ''),
+ json_last_error_msg(),
+ $json
+ );
+ }
+
+ $filter = new FilterAnd();
+
+ if (property_exists($data, 'host')) {
+ // Note: we can' support regexp right now, but we replace '.*' to a normal wildcard
+ $data->host = str_replace('.*', '*', $data->host);
+ if ($data->host === '*') {
+ $data->host = null;
+ }
+ } else {
+ $data->host = $host;
+ }
+ if ($data->host !== null) {
+ $filter->andFilter(Filter::expression('host_name', '=', $data->host));
+ }
+
+ static::handleArray($filter, $data, 'programInclusion', 'program');
+ static::handleArray($filter, $data, 'programExclusion', 'program', '!=');
+
+ static::handleArray($filter, $data, 'priorityExclusion', 'priority', '!=');
+ static::handleArray($filter, $data, 'sourceExclusion', 'source', '!=');
+ static::handleArray($filter, $data, 'facilityExclusion', 'facility', '!=');
+
+ // TODO: msg - when really needed
+ // TODO: startTime - when really needed
+
+ // Note: any other field or data part gets ignored...
+
+ return $filter;
+ }
+
+ protected static function handleArray(Filter $filter, $data, $property, $filterAttr, $op = '=')
+ {
+ if (property_exists($data, $property) && ! empty($data->$property)) {
+ if ($op === '!=') {
+ $subFilter = $filter;
+ } else {
+ $subFilter = new FilterOr;
+ }
+
+ /*
+ if (is_array($data->$property) && count($data->$property) === 1) {
+ $data->$property = current($data->$property);
+ }
+ */
+ if (! is_array($data->$property)) {
+ $data->$property = array($data->$property);
+ }
+ foreach ($data->$property as $val) {
+ $subFilter->addFilter(Filter::expression($filterAttr, $op, $val));
+ }
+
+ if ($subFilter !== $filter) {
+ $filters = $subFilter->filters();
+ if ($filter->isChain() && count($filters) > 1) {
+ $filter->andFilter($subFilter);
+ } else {
+ $filter->andFilter(current($filters));
+ }
+ }
+ }
+ }
+
+ /**
+ * @author partially by NikiC https://stackoverflow.com/users/385378/nikic
+ * @source partially from https://stackoverflow.com/a/20440596/449813
+ *
+ * @param $json string
+ *
+ * @return string
+ */
+ public static function fixJSONQuotes($json)
+ {
+ // fix unquoted identifiers
+ $json = preg_replace('/([{,]+)(\s*)([^"]+?)\s*:/', '$1"$3":', $json);
+
+ $regex = <<<'REGEX'
+~
+ "[^"\\]*(?:\\.|[^"\\]*)*"
+ (*SKIP)(*F)
+ | '([^'\\]*(?:\\.|[^'\\]*)*)'
+~x
+REGEX;
+
+ return preg_replace_callback($regex, function ($matches) {
+ return '"' . preg_replace('~\\\\.(*SKIP)(*F)|"~', '\\"', $matches[1]) . '"';
+ }, $json);
+ }
+
+ /**
+ * Basic check if it looks like a JSON filter
+ *
+ * @param $string
+ *
+ * @return bool
+ */
+ static public function isJsonFilter($string)
+ {
+ if (! $string || ! is_string($string)) {
+ return false;
+ }
+
+ $string = trim($string);
+ if (empty($string)) {
+ return false;
+ }
+
+ if (preg_match('/^\{.*\}$/s', $string)) {
+ // looks like JSON data
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/library/Eventdb/Event.php b/library/Eventdb/Event.php
new file mode 100644
index 0000000..a3bba1a
--- /dev/null
+++ b/library/Eventdb/Event.php
@@ -0,0 +1,120 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb;
+
+use ArrayObject;
+
+class Event extends ArrayObject
+{
+ public static $facilities = array(
+ 0 => 'kernel messages',
+ 1 => 'user-level messages',
+ 2 => 'mail system',
+ 3 => 'system daemons',
+ 4 => 'security/authorization messages',
+ 5 => 'messages generated internally by syslogd',
+ 6 => 'line printer subsystem',
+ 7 => 'network news subsystem',
+ 8 => 'UUCP subsystem',
+ 9 => 'clock daemon',
+ 10 => 'security/authorization messages',
+ 11 => 'FTP daemon',
+ 12 => 'NTP subsystem',
+ 13 => 'log audit',
+ 14 => 'log alert',
+ 15 => 'clock daemon',
+ 16 => 'local use 0',
+ 17 => 'local use 1',
+ 18 => 'local use 2',
+ 19 => 'local use 3',
+ 20 => 'local use 4',
+ 21 => 'local use 5',
+ 22 => 'local use 6',
+ 23 => 'local use 7'
+ );
+
+ public static $priorities = array(
+ 0 => 'emergency',
+ 1 => 'alert',
+ 2 => 'critical',
+ 3 => 'error',
+ 4 => 'warning',
+ 5 => 'notice',
+ 6 => 'info',
+ 7 => 'debug'
+ );
+
+ public static $types = array(
+ 0 => 'syslog',
+ 1 => 'snmp',
+ 2 => 'mail'
+ );
+
+ public static $typeIcons = array(
+ '_default' => 'help',
+ 'syslog' => 'doc-text',
+ 'snmp' => 'plug',
+ 'mail' => 'bell',
+ );
+
+ public function __construct($data)
+ {
+ parent::__construct($data, ArrayObject::ARRAY_AS_PROPS);
+ }
+
+ public function offsetGet($index)
+ {
+ if (! $this->offsetExists($index)) {
+ return null;
+ }
+ $getter = 'get' . ucfirst($index);
+ if (method_exists($this, $getter)) {
+ return $this->$getter();
+ }
+ return parent::offsetGet($index);
+ }
+
+ public function getAck()
+ {
+ return (bool) parent::offsetGet('ack');
+ }
+
+ public function getFacility()
+ {
+ $facility = (int) parent::offsetGet('facility');
+ return array_key_exists($facility, static::$facilities) ? static::$facilities[$facility] : $facility;
+ }
+
+ public function getPriority()
+ {
+ $priority = (int) parent::offsetGet('priority');
+ return array_key_exists($priority, static::$priorities) ? static::$priorities[$priority] : $priority;
+ }
+
+ public function getType()
+ {
+ $type = (int) parent::offsetGet('type');
+ return array_key_exists($type, static::$types) ? static::$types[$type] : $type;
+ }
+
+ public function getTypeIcon()
+ {
+ if (array_key_exists($type = $this->getType(), static::$typeIcons)) {
+ return static::$typeIcons[$type];
+ } else {
+ return static::$typeIcons['_default'];
+ }
+ }
+
+ public static function fromData($data)
+ {
+ return new static($data);
+ }
+
+ public static function getPriorityId($priorityName)
+ {
+ $priorities = array_flip(static::$priorities);
+ return $priorities[$priorityName];
+ }
+}
diff --git a/library/Eventdb/Eventdb.php b/library/Eventdb/Eventdb.php
new file mode 100644
index 0000000..b885258
--- /dev/null
+++ b/library/Eventdb/Eventdb.php
@@ -0,0 +1,184 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb;
+
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Repository\DbRepository;
+use Icinga\Repository\RepositoryQuery;
+
+class Eventdb extends DbRepository
+{
+ /**
+ * {@inheritdoc}
+ */
+ const DATETIME_FORMAT = 'Y-m-d G:i:s';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableAliases = array(
+ 'comment' => 'c',
+ 'event' => 'e',
+ );
+
+ /**
+ * Default query columns
+ *
+ * @var array
+ */
+ protected static $defaultQueryColumns = array(
+ 'event' => array(
+ 'id',
+ 'host_name',
+ 'host_address',
+ 'type',
+ 'facility',
+ 'priority',
+ 'program',
+ 'message',
+ 'alternative_message',
+ 'ack',
+ 'created',
+ 'modified',
+ ),
+ 'comment' => array(
+ 'id',
+ 'event_id',
+ 'type',
+ 'message',
+ 'created',
+ 'modified',
+ 'user'
+ )
+ );
+
+ protected static $edbcQueryColumns = array(
+ 'event' => array(
+ 'group_active',
+ 'group_id',
+ 'group_count',
+ 'group_leader',
+ 'group_autoclear',
+ 'flags',
+ 'alternative_message'
+ )
+ );
+
+ /** @var bool */
+ protected $hasCorrelatorExtensions = null;
+
+ /**
+ * Checks if Event repository has EDBC columns
+ *
+ * @return bool
+ */
+ public function hasCorrelatorExtensions()
+ {
+ if ($this->hasCorrelatorExtensions === null) {
+ $dba = $this->getDataSource()->getDbAdapter();
+ $result = $dba->fetchRow("SHOW COLUMNS FROM `event` LIKE 'group_leader'");
+
+ $this->hasCorrelatorExtensions = ! ! $result;
+ }
+ return $this->hasCorrelatorExtensions;
+ }
+
+ public function filterGroups(RepositoryQuery $query)
+ {
+ if ($this->hasCorrelatorExtensions()) {
+ $query->addFilter(Filter::matchAny(
+ Filter::expression('group_leader', '=', -1),
+ new FilterExpression('group_leader', 'IS', new \Zend_Db_Expr('NULL'))
+ ));
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function initializeQueryColumns()
+ {
+ $additionalColumns = Config::module('eventdb', 'columns')->keys();
+ $queryColumns = static::$defaultQueryColumns;
+ if ($this->hasCorrelatorExtensions()) {
+ foreach (static::$edbcQueryColumns as $table => $fields) {
+ if (array_key_exists($table, $queryColumns)) {
+ $queryColumns[$table] = array_merge($queryColumns[$table], $fields);
+ } else {
+ $queryColumns[$table] = $fields;
+ }
+ }
+ }
+ if ($additionalColumns !== null) {
+ $eventColumns = $queryColumns['event'];
+ $queryColumns['event'] = array_merge($eventColumns, array_diff($additionalColumns, $eventColumns));
+ }
+ return $queryColumns;
+ }
+
+ /**
+ * Create and return a new instance of the Eventdb
+ *
+ * @param ConfigObject $config The configuration to use, otherwise the module's configuration
+ *
+ * @return static
+ *
+ * @throws ConfigurationError In case no resource has been configured in the module's configuration
+ */
+ public static function fromConfig(ConfigObject $config = null)
+ {
+ if ($config === null) {
+ $moduleConfig = Config::module('eventdb');
+ if (($resourceName = $moduleConfig->get('backend', 'resource')) === null) {
+ throw new ConfigurationError(
+ mt('eventdb', 'You need to configure a resource to access the EventDB database first')
+ );
+ }
+
+ $resource = ResourceFactory::create($resourceName);
+ } else {
+ $resource = ResourceFactory::createResource($config);
+ }
+
+ return new static($resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function initializeConversionRules()
+ {
+ return array('event' => array('host_address' => 'ip_address'));
+ }
+
+ /**
+ * Convert an IP address into its human-readable form
+ *
+ * @param string $rawAddress
+ *
+ * @return string
+ */
+ protected function retrieveIpAddress($rawAddress)
+ {
+ return $rawAddress === null ? null : inet_ntop($rawAddress);
+ }
+
+ /**
+ * Convert an IP address into its binary form
+ *
+ * @param string $address
+ *
+ * @return string
+ */
+ protected function persistIpAddress($address)
+ {
+ return $address === null ? null : inet_pton($address);
+ }
+}
diff --git a/library/Eventdb/EventdbController.php b/library/Eventdb/EventdbController.php
new file mode 100644
index 0000000..538553e
--- /dev/null
+++ b/library/Eventdb/EventdbController.php
@@ -0,0 +1,181 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\QueryInterface;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\Json\JsonEncodeException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Controller;
+use Icinga\Web\View;
+
+class EventdbController extends Controller
+{
+ /** @var View */
+ public $view;
+
+ /** @var MonitoringBackend */
+ protected $monitoringBackend;
+
+ /**
+ * Get the EventDB repository
+ *
+ * @return Eventdb
+ */
+ protected function getDb()
+ {
+ return Eventdb::fromConfig();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRestrictions($name, $permission = null)
+ {
+ $restrictions = array();
+ if ($this->Auth()->isAuthenticated()) {
+ foreach ($this->Auth()->getUser()->getRoles() as $role) {
+ if ($permission !== null && ! in_array($permission, $role->getPermissions())) {
+ continue;
+ }
+ $restrictionsFromRole = $role->getRestrictions($name);
+ if (empty($restrictionsFromRole)) {
+ $restrictions = array();
+ break;
+ } else {
+ if (! is_array($restrictionsFromRole)) {
+ $restrictionsFromRole = array($restrictionsFromRole);
+ }
+ $restrictions = array_merge($restrictions, array_values($restrictionsFromRole));
+ }
+ }
+ }
+ return $restrictions;
+ }
+
+ /**
+ * Retrieves the Icinga MonitoringBackend
+ *
+ * @param string|null $name
+ *
+ * @return MonitoringBackend
+ * @throws IcingaException When monitoring is not enabled
+ */
+ protected function monitoringBackend($name = null)
+ {
+ if ($this->monitoringBackend === null) {
+ if (! Icinga::app()->getModuleManager()->hasEnabled('monitoring')) {
+ throw new IcingaException('The module "monitoring" must be enabled and configured!');
+ }
+ $this->monitoringBackend = MonitoringBackend::instance($name);
+ }
+ return $this->monitoringBackend;
+ }
+
+ protected function setViewScript($name)
+ {
+ $this->_helper->viewRenderer->setNoController(true);
+ $this->_helper->viewRenderer->setScriptAction($name);
+ }
+
+ protected function isFormatRequest()
+ {
+ return $this->hasParam('format');
+ }
+
+ protected function isApiRequest()
+ {
+ $format = $this->getParam('format');
+ $header = $this->getRequest()->getHeader('Accept');
+
+ if ($format === 'json' || preg_match('#application/json(;.+)?#', $header)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function isTextRequest()
+ {
+ $format = $this->getParam('format');
+ if ($format === 'text' || $this->isPlainTextRequest()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function isPlainTextRequest()
+ {
+ $header = $this->getRequest()->getHeader('Accept');
+ if ($header !== null && preg_match('#text/plain#', $header)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Send the user a summary of SQL queries
+ *
+ * @param array|QueryInterface $queries
+ */
+ protected function sendSqlSummary($queries)
+ {
+ if (! is_array($queries)) {
+ $queries = array($queries);
+ }
+
+ $str = '';
+ foreach ($queries as $query) {
+ if ($query !== null) {
+ $str .= wordwrap($query) . "\n\n";
+ }
+ }
+
+ $this->sendText($str);
+ }
+
+ /**
+ * Output JSON data to the requester
+ *
+ * @param mixed $data
+ * @param int $options
+ * @param int $depth
+ *
+ * @throws JsonEncodeException
+ */
+ protected function sendJson($data, $options = 0, $depth = 100)
+ {
+ header('Content-Type: application/json');
+
+ if (defined('JSON_PARTIAL_OUTPUT_ON_ERROR')) {
+ $options |= JSON_PARTIAL_OUTPUT_ON_ERROR;
+ }
+
+ $output = json_encode($data, $options, $depth);
+ if (! $output && json_last_error() !== null) {
+ throw new JsonEncodeException('JSON error: ' . json_last_error_msg());
+ }
+ echo $output;
+ exit;
+ }
+
+ protected function sendText($str, $script = null)
+ {
+ if ($this->isPlainTextRequest()) {
+ if ($script !== null) {
+ echo $this->view->render($this->getViewScript($script, true));
+ } else {
+ echo $str;
+ }
+ exit;
+ } else {
+ $this->view->text = $str;
+ $this->view->partial = $script;
+ $this->setViewScript('format/text');
+ }
+ }
+}
diff --git a/library/Eventdb/Hook/DetailviewExtensionHook.php b/library/Eventdb/Hook/DetailviewExtensionHook.php
new file mode 100644
index 0000000..9fee702
--- /dev/null
+++ b/library/Eventdb/Hook/DetailviewExtensionHook.php
@@ -0,0 +1,124 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Hook;
+
+use Icinga\Application\ClassLoader;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Eventdb\Event;
+use Icinga\Web\View;
+
+/**
+ * Base class for hooks extending the detail view of events
+ *
+ * Extend this class if you want to extend the detail view of events with custom HTML.
+ */
+abstract class DetailviewExtensionHook
+{
+ /**
+ * The view the generated HTML will be included in
+ *
+ * @var View
+ */
+ private $view;
+
+ /**
+ * The module of the derived class
+ *
+ * @var Module
+ */
+ private $module;
+
+ /**
+ * Create a new hook
+ *
+ * @see init() For hook initialization.
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function for hook initialization, e.g. loading the hook's config
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Shall return valid HTML to include in the detail view
+ *
+ * @param Event $event The event to generate HTML for
+ *
+ * @return string
+ */
+ abstract public function getHtmlForEvent(Event $event);
+
+ /**
+ * Shall return valid HTML to include in the multi-select view for events
+ *
+ * @param Event[] $events The events to generate HTML for
+ *
+ * @return string
+ */
+ public function getHtmlForEvents($events)
+ {
+ return '';
+ }
+
+ /**
+ * Get {@link view}
+ *
+ * @return View
+ */
+ public function getView()
+ {
+ return $this->view;
+ }
+
+ /**
+ * Set {@link view}
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView($view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Get the module of the derived class
+ *
+ * @return Module
+ */
+ public function getModule()
+ {
+ if ($this->module === null) {
+ $class = get_class($this);
+ if (ClassLoader::classBelongsToModule($class)) {
+ $this->module = Icinga::app()->getModuleManager()->getModule(ClassLoader::extractModuleName($class));
+ }
+ }
+
+ return $this->module;
+ }
+
+ /**
+ * Set the module of the derived class
+ *
+ * @param Module $module
+ *
+ * @return $this
+ */
+ public function setModule(Module $module)
+ {
+ $this->module = $module;
+
+ return $this;
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php b/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php
new file mode 100644
index 0000000..e03f401
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Module\Eventdb\Eventdb;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Url;
+
+/**
+ * Available in icingaweb2 after 2.5.0
+ */
+class DetailviewExtension extends DetailviewExtensionHook
+{
+ public function getHtmlForObject(MonitoredObject $object)
+ {
+ if (! Auth::getInstance()->hasPermission('eventdb/events')) {
+ return '';
+ }
+
+ $config = static::config();
+
+ if ($config->get('detailview_disable') === '1') {
+ return '';
+ }
+
+ $actions = clone EventdbActionHook::getActions($object);
+ if (! $actions->hasRenderableItems()) {
+ // no actions -> no EventDB
+ return '';
+ }
+
+ $htm = '<h2>EventDB</h2>';
+
+ $htm .= '<div class="quick-actions">';
+ $actions->setLayout(Navigation::LAYOUT_TABS);
+ $htm .= $actions->render();
+ $htm .= '</div>';
+
+ $url = Url::fromPath('eventdb/events', array('host_name' => $object->host_name));
+
+ $customFilter = EventdbActionHook::getCustomFilter($object);
+ if ($customFilter === null) {
+ $customFilter = new FilterAnd;
+ }
+ $detailview_filter = $config->get('detailview_filter', 'ack=0');
+ if ($detailview_filter !== null) {
+ $customFilter = $customFilter->andFilter(Filter::fromQueryString($detailview_filter));
+ }
+
+ $htm .= sprintf(
+ '<div class="container" data-last-update="-1" data-icinga-url="%s" data-icinga-refresh="60">',
+ $url->with(array(
+ 'sort' => 'priority',
+ 'dir' => 'asc',
+ 'view' => 'compact',
+ 'limit' => 5,
+ ))->addFilter($customFilter)
+ );
+ $htm .= '<p class="progress-label">' . mt('eventdb', 'Loading') . '<span>.</span><span>.</span><span>.</span></p>';
+ $htm .= '</div>';
+
+ return $htm;
+ }
+
+ protected function eventDb()
+ {
+ return Eventdb::fromConfig();
+ }
+
+ protected static function config()
+ {
+ return Config::module('eventdb')->getSection('monitoring');
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php b/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php
new file mode 100644
index 0000000..1eb968d
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php
@@ -0,0 +1,182 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterParseException;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Eventdb\Data\LegacyFilterParser;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+
+class EventdbActionHook
+{
+ protected static $wantCache = true;
+
+ protected static $cachedNav = array();
+
+ protected static $customFilters = array();
+
+ public static function wantCache($bool=true)
+ {
+ static::$wantCache = $bool;
+ }
+
+ /**
+ * @param MonitoredObject $object
+ * @param bool $no_cache
+ *
+ * @return null|Filter
+ */
+ public static function getCustomFilter(MonitoredObject $object)
+ {
+ if (! Auth::getInstance()->hasPermission('eventdb/events')) {
+ return null;
+ }
+
+ $objectKey = static::getObjectKey($object);
+
+ // check cache if the filter already have been rendered
+ if (static::$wantCache && array_key_exists($objectKey, self::$customFilters)) {
+ return self::$customFilters[$objectKey];
+ }
+
+ $config = static::config();
+ $custom_var = $config->get('custom_var', null);
+
+ $service = null;
+ $edb_filter = null;
+ if ($custom_var !== null && $object instanceof Service) {
+ $edb_filter = $object->{'_service_' . $custom_var . '_filter'};
+ $service = $object->service_description;
+ } elseif ($custom_var !== null && $object instanceof Host) {
+ $edb_filter = $object->{'_host_' . $custom_var . '_filter'};
+ }
+
+ $customFilter = null;
+ if ($edb_filter !== null) {
+ if (LegacyFilterParser::isJsonFilter($edb_filter)) {
+ try {
+ $customFilter = LegacyFilterParser::parse(
+ $edb_filter,
+ $object->host_name,
+ $service
+ );
+ } catch (InvalidPropertyException $e) {
+ Logger::warning($e->getMessage());
+ }
+ } else {
+ try {
+ $customFilter = Filter::fromQueryString($edb_filter);
+
+ if (! in_array('host_name', $customFilter->listFilteredColumns())) {
+ $customFilter = $customFilter->andFilter(Filter::expression('host_name', '=', $object->host_name));
+ }
+ } catch (FilterParseException $e) {
+ Logger::warning('Could not parse custom EventDB filter: %s (%s)', $edb_filter, $e->getMessage());
+ }
+ }
+ }
+
+ return self::$customFilters[$objectKey] = $customFilter;
+ }
+
+ /**
+ * @param MonitoredObject $object Host or Service to render for
+ * @param bool $no_cache Only for testing - to avoid caching
+ *
+ * @return array|Navigation
+ */
+ public static function getActions(MonitoredObject $object)
+ {
+ if (! Auth::getInstance()->hasPermission('eventdb/events')) {
+ return array();
+ }
+
+ $objectKey = static::getObjectKey($object);
+
+ // check cache if the buttons already have been rendered
+ if (static::$wantCache && array_key_exists($objectKey, self::$cachedNav)) {
+ return self::$cachedNav[$objectKey];
+ }
+
+ $nav = new Navigation();
+
+ $config = static::config();
+
+ $custom_var = $config->get('custom_var', null);
+
+ $edb_cv = null;
+ $always_on = null;
+
+ if ($custom_var !== null && $object instanceof Service) {
+ $edb_cv = $object->{'_service_' . $custom_var};
+ $always_on = $config->get('always_on_service', 0);
+ } elseif ($custom_var !== null && $object instanceof Host) {
+ $edb_cv = $object->{'_host_' . $custom_var};
+ $always_on = $config->get('always_on_host', 0);
+ }
+
+ $customFilter = static::getCustomFilter($object);
+ if ($customFilter !== null) {
+ $params = UrlParams::fromQueryString($customFilter->toQueryString());
+ $nav->addItem(
+ 'events_filtered',
+ array(
+ 'label' => mt('eventdb', 'Filtered events'),
+ 'url' => Url::fromPath('eventdb/events')->setParams($params),
+ 'icon' => 'tasks',
+ 'class' => 'action-link',
+ 'priority' => 1
+ )
+ );
+ }
+
+ // show access to all events, if (or)
+ // - custom_var is not configured
+ // - always_on is configured
+ // - custom_var is configured and set on object (to any value)
+ if ($custom_var === null || ! empty($edb_cv) || ! empty($always_on)) {
+ $nav->addItem(
+ 'events',
+ array(
+ 'label' => mt('eventdb', 'All events for host'),
+ 'url' => Url::fromPath(
+ 'eventdb/events',
+ array(
+ 'host_name' => $object->host_name,
+ )
+ ),
+ 'icon' => 'tasks',
+ 'class' => 'action-link',
+ 'priority' => 99
+ )
+ );
+ }
+
+ return self::$cachedNav[$objectKey] = $nav;
+ }
+
+ protected static function getObjectKey(MonitoredObject $object)
+ {
+ $type = $object->getType();
+ $objectKey = sprintf('%!%', $type, $object->host_name);
+ if ($type === 'service') {
+ $objectKey .= '!' . $object->service_description;
+ }
+ return $objectKey;
+ }
+
+ protected static function config()
+ {
+ return Config::module('eventdb')->getSection('monitoring');
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/HostActions.php b/library/Eventdb/ProvidedHook/Monitoring/HostActions.php
new file mode 100644
index 0000000..caf33e3
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/HostActions.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\HostActionsHook;
+use Icinga\Module\Monitoring\Object\Host;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForHost(Host $host)
+ {
+ return EventdbActionHook::getActions($host);
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php b/library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php
new file mode 100644
index 0000000..61a7a87
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
+use Icinga\Module\Monitoring\Object\Service;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForService(Service $service)
+ {
+ return EventdbActionHook::getActions($service);
+ }
+}
diff --git a/library/Eventdb/Test/BaseTestCase.php b/library/Eventdb/Test/BaseTestCase.php
new file mode 100644
index 0000000..d24ea49
--- /dev/null
+++ b/library/Eventdb/Test/BaseTestCase.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Application\Icinga;
+use PHPUnit_Framework_TestCase;
+
+abstract class BaseTestCase extends PHPUnit_Framework_TestCase
+{
+ private static $app;
+
+ private $db;
+
+ public function setUp()
+ {
+ $this->app();
+ }
+
+ protected function app()
+ {
+ if (self::$app === null) {
+ self::$app = Icinga::app();
+ }
+
+ return self::$app;
+ }
+}
diff --git a/library/Eventdb/Test/Bootstrap.php b/library/Eventdb/Test/Bootstrap.php
new file mode 100644
index 0000000..848b360
--- /dev/null
+++ b/library/Eventdb/Test/Bootstrap.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Application\EmbeddedWeb;
+use Icinga\Authentication\Auth;
+use Icinga\User;
+
+class Bootstrap
+{
+ public static function web($basedir = null)
+ {
+ error_reporting(E_ALL | E_STRICT);
+ if ($basedir === null) {
+ $basedir = dirname(dirname(dirname(__DIR__)));
+ }
+ $testsDir = $basedir . '/test';
+ require_once 'Icinga/Application/EmbeddedWeb.php';
+
+ if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) {
+ $configDir = $_SERVER['ICINGAWEB_CONFIGDIR'];
+ } else {
+ $configDir = $testsDir . '/config';
+ }
+
+ EmbeddedWeb::start($testsDir, $configDir)
+ ->getModuleManager()
+ ->loadModule('eventdb', $basedir)
+ ->loadModule('monitoring', $basedir . '/vendor/icingaweb2/modules/monitoring');
+
+ $user = new User('icingaadmin');
+ $user->setPermissions(array('*'));
+ Auth::getInstance()->setAuthenticated($user);
+ }
+}
diff --git a/library/Eventdb/Test/PseudoHost.php b/library/Eventdb/Test/PseudoHost.php
new file mode 100644
index 0000000..7ebcd8c
--- /dev/null
+++ b/library/Eventdb/Test/PseudoHost.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Module\Monitoring\Object\Host;
+
+class PseudoHost extends Host
+{
+ public function provideCustomVars($vars)
+ {
+ $this->properties = new \stdClass;
+ $this->hostVariables = $this->customvars = $vars;
+ return $this;
+ }
+}
diff --git a/library/Eventdb/Test/PseudoMonitoringBackend.php b/library/Eventdb/Test/PseudoMonitoringBackend.php
new file mode 100644
index 0000000..3ad6de5
--- /dev/null
+++ b/library/Eventdb/Test/PseudoMonitoringBackend.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class PseudoMonitoringBackend extends MonitoringBackend
+{
+ public static function dummy()
+ {
+ return new static('dummy', new ConfigObject());
+ }
+}
diff --git a/library/Eventdb/Test/PseudoService.php b/library/Eventdb/Test/PseudoService.php
new file mode 100644
index 0000000..737a8ac
--- /dev/null
+++ b/library/Eventdb/Test/PseudoService.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Module\Monitoring\Object\Service;
+
+class PseudoService extends Service
+{
+ public function provideCustomVars($vars)
+ {
+ $this->properties = new \stdClass;
+ $this->serviceVariables = $this->customvars = $vars;
+ $this->hostVariables = array();
+ return $this;
+ }
+}
diff --git a/library/Eventdb/Web/EventdbOutputFormat.php b/library/Eventdb/Web/EventdbOutputFormat.php
new file mode 100644
index 0000000..ceec1ce
--- /dev/null
+++ b/library/Eventdb/Web/EventdbOutputFormat.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Web;
+
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use Icinga\Web\Widget\Tabs;
+
+class EventdbOutputFormat extends OutputFormat
+{
+ /**
+ * TEXT output type
+ */
+ const TYPE_TEXT = 'text';
+
+ /**
+ * Types that are disabled by default
+ *
+ * @var array
+ */
+ protected static $disabledTypes = array(self::TYPE_PDF, self::TYPE_CSV);
+
+ /**
+ * Types that are enabled in addition to default
+ *
+ * @var array
+ */
+ protected $enable = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($disabled = array(), $enable = array())
+ {
+ $this->enable = $enable;
+ $disabled = array_merge(static::$disabledTypes, $disabled);
+ parent::__construct($disabled);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedTypes()
+ {
+ $supported = parent::getSupportedTypes();
+
+ if (in_array(self::TYPE_TEXT, $this->enable)) {
+ $supported[self::TYPE_TEXT] = array(
+ 'name' => 'text',
+ 'label' => mt('eventdb', 'Text'),
+ 'icon' => 'doc-text',
+ 'urlParams' => array('format' => 'text'),
+ );
+ }
+
+ return $supported;
+ }
+
+ public function apply(Tabs $tabs)
+ {
+ parent::apply($tabs);
+
+ if ($textTab = $tabs->get(self::TYPE_TEXT)) {
+ $textTab->setTargetBlank(false);
+ }
+ }
+}