diff options
76 files changed, 5060 insertions, 0 deletions
diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..eedb019 --- /dev/null +++ b/.mailmap @@ -0,0 +1,6 @@ +<markus.frosch@icinga.com> <markus.frosch@netways.de> +<markus.frosch@icinga.com> <markus@lazyfrosch.de> +<michael.friedrich@icinga.com> <michael.friedrich@netways.de> +Eric Lippmann <eric.lippmann@icinga.com> <eric.lippmann@netways.de> +Eric Lippmann <eric.lippmann@icinga.com> <lippserd@googlemail.com> +<johannes.meyer@icinga.com> <johannes.meyer@netways.de> diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e0b0b51 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: php +php: + - '5.4' + - '5.5' + - '5.6' + - '7.0' + - '7.1' + - nightly + +cache: + directories: + - vendor + +matrix: + fast_finish: true + allow_failures: + - php: nightly + +branches: + only: + - master + - /^v\d/ + +before_script: +- ./test/setup_vendor.sh + +script: + - php vendor/phpunit.phar @@ -0,0 +1,4 @@ +Alexander A. Klimov <alexander.klimov@icinga.com> +Eric Lippmann <eric.lippmann@icinga.com> +Markus Frosch <markus.frosch@icinga.com> +Michael Friedrich <michael.friedrich@icinga.com> diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0302b1f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Change Log + +## [v1.3.0](https://github.com/Icinga/icingaweb2-module-eventdb/tree/v1.3.0) (2018-01-25) +[Full Changelog](https://github.com/Icinga/icingaweb2-module-eventdb/compare/v1.2.0...v1.3.0) + +**Implemented enhancements:** + +- Links in Event text [\#24](https://github.com/Icinga/icingaweb2-module-eventdb/issues/24) +- Severity Widget for fast clicking users [\#23](https://github.com/Icinga/icingaweb2-module-eventdb/issues/23) +- Add default\_filter config option for the menu url [\#27](https://github.com/Icinga/icingaweb2-module-eventdb/pull/27) ([lazyfrosch](https://github.com/lazyfrosch)) +- Add DetailviewExtensionHook for detail and multi-select [\#26](https://github.com/Icinga/icingaweb2-module-eventdb/pull/26) ([lazyfrosch](https://github.com/lazyfrosch)) + +**Fixed bugs:** + +- Detailview extension shown despite customvar not present [\#25](https://github.com/Icinga/icingaweb2-module-eventdb/issues/25) +- Pagination issue [\#22](https://github.com/Icinga/icingaweb2-module-eventdb/issues/22) + +## [v1.2.0](https://github.com/Icinga/icingaweb2-module-eventdb/tree/v1.2.0) (2017-08-17) +[Full Changelog](https://github.com/Icinga/icingaweb2-module-eventdb/compare/v1.1.0...v1.2.0) + +**Implemented enhancements:** + +- Add more output / format options [\#21](https://github.com/Icinga/icingaweb2-module-eventdb/pull/21) ([lazyfrosch](https://github.com/lazyfrosch)) + +## [v1.1.0](https://github.com/Icinga/icingaweb2-module-eventdb/tree/v1.1.0) (2017-08-16) +[Full Changelog](https://github.com/Icinga/icingaweb2-module-eventdb/compare/v1.0.0...v1.1.0) + +**Implemented enhancements:** + +- Add screenshot to README.md [\#1](https://github.com/Icinga/icingaweb2-module-eventdb/issues/1) +- Support EDBC extensions [\#15](https://github.com/Icinga/icingaweb2-module-eventdb/issues/15) +- Event list should be auto-reloading [\#13](https://github.com/Icinga/icingaweb2-module-eventdb/issues/13) +- Add integration to host and service [\#12](https://github.com/Icinga/icingaweb2-module-eventdb/issues/12) +- Add German translation [\#9](https://github.com/Icinga/icingaweb2-module-eventdb/issues/9) +- Popup Tool Tips for Action Buttons in Interface missing [\#7](https://github.com/Icinga/icingaweb2-module-eventdb/issues/7) +- Fuzzy link to hostnames [\#5](https://github.com/Icinga/icingaweb2-module-eventdb/issues/5) +- Add EDBC extensions [\#20](https://github.com/Icinga/icingaweb2-module-eventdb/pull/20) ([lazyfrosch](https://github.com/lazyfrosch)) +- Add detailview extension [\#19](https://github.com/Icinga/icingaweb2-module-eventdb/pull/19) ([lazyfrosch](https://github.com/lazyfrosch)) +- Integration into monitoring [\#16](https://github.com/Icinga/icingaweb2-module-eventdb/pull/16) ([lazyfrosch](https://github.com/lazyfrosch)) +- Improve list and detail UI [\#14](https://github.com/Icinga/icingaweb2-module-eventdb/pull/14) ([lazyfrosch](https://github.com/lazyfrosch)) +- Improve usability of priority/severity widget [\#11](https://github.com/Icinga/icingaweb2-module-eventdb/pull/11) ([lazyfrosch](https://github.com/lazyfrosch)) +- DB repository: add convert rules for event.host\_address [\#6](https://github.com/Icinga/icingaweb2-module-eventdb/pull/6) ([Al2Klimov](https://github.com/Al2Klimov)) + +**Fixed bugs:** + +- Acknowledgement only available in multi selection [\#8](https://github.com/Icinga/icingaweb2-module-eventdb/issues/8) + +**Closed issues:** + +- Configuration for extra types [\#18](https://github.com/Icinga/icingaweb2-module-eventdb/issues/18) +- Add Host Column to Overview [\#2](https://github.com/Icinga/icingaweb2-module-eventdb/issues/2) +- The content of 'Host Address' is not readable [\#4](https://github.com/Icinga/icingaweb2-module-eventdb/issues/4) + +**Merged pull requests:** + +- Add basic phpunit environment [\#17](https://github.com/Icinga/icingaweb2-module-eventdb/pull/17) ([lazyfrosch](https://github.com/lazyfrosch)) +- Add GitHub issue template [\#10](https://github.com/Icinga/icingaweb2-module-eventdb/pull/10) ([dnsmichi](https://github.com/dnsmichi)) + + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
\ No newline at end of file @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License.
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4edb9e1 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# EventDB Module for Icinga Web 2 + +[![Build Status](https://travis-ci.org/Icinga/icingaweb2-module-eventdb.svg?branch=master)](https://travis-ci.org/Icinga/icingaweb2-module-eventdb) +[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-eventdb.svg)](https://github.com/Icinga/icingaweb2-module-eventdb) + +![Icinga Logo](https://www.icinga.com/wp-content/uploads/2014/06/icinga_logo.png) + +* [About](#about) +* [Requirements](#requirements) +* [Getting Started](#getting-started) +* [Documentation](#documentation) +* [License](#license) + +## About + +With the EventDB Module you can browse, comment and acknowledge events collected +by [EventDB](https://git.netways.org/eventdb/eventdb) easily in +[Icinga Web 2](https://www.icinga.org/products/icinga-web-2/). + +![screenshot](doc/screenshots/overview.png) + +Also see [Screenshots](doc/10-Screenshots.md) in documentation. + +## Requirements + +* Icinga Web 2 +* A database with events collected by EventDB + +## Getting started + +Install and enable the module, then go to [Configuration](doc/02-Configuration.md) +to set up the essentials. + +## Documentation + +* [Configuration](doc/02-Configuration.md) +* [Custom variables](doc/03-CustomVars.md) +* [Security](doc/09-Security.md) +* [Screenshots](doc/10-Screenshots.md) + +## License + + Copyright (C) 2016-2017 Icinga Development Team + 2016-2017 Eric Lippmann <eric.lippmann@icinga.com> + 2017 Markus Frosch <markus.frosch@icinga.com> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..57cd2ea --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,63 @@ +# Release Workflow + +Specify the release version. + +``` +VERSION=1.3.0 +``` + +## Issues + +Check issues at https://github.com/Icinga/icingaweb2-module-eventdb + +## Authors + +Update the [.mailmap](.mailmap) and [AUTHORS](AUTHORS) files: + +``` +git checkout master +git log --use-mailmap | grep ^Author: | cut -f2- -d' ' | sort | uniq > AUTHORS +``` + +## Update metadata + +Edit and update [module.info](module.info). + +## Changelog + +Update the [CHANGELOG.md](CHANGELOG.md) file. + +Uses [github_changelog_generator](https://github.com/skywinder/github-changelog-generator) + +``` +export CHANGELOG_GITHUB_TOKEN=xxx +github_changelog_generator --future-release v$VERSION +``` + +Check if the file has been updated correctly. + +## Git Tag + +Commit these changes to the "master" branch: + +``` +git commit -v -a -m "Release version $VERSION" +git push origin master +``` + +And tag it with a signed tag: + +``` +git tag -s -m "Version $VERSION" v$VERSION +``` + +Push the tag. + +``` +git push --tags +``` + +## GitHub Release + +Create a new release for the newly created Git tag. +https://github.com/Icinga/icingaweb2-module-eventdb/releases diff --git a/application/controllers/CommentsController.php b/application/controllers/CommentsController.php new file mode 100644 index 0000000..a371c7b --- /dev/null +++ b/application/controllers/CommentsController.php @@ -0,0 +1,17 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Controllers; + +use Icinga\Module\Eventdb\EventdbController; + +class CommentsController extends EventdbController +{ + /** + * @deprecated Moved to eventdb/events/details + */ + public function newAction() + { + $this->redirectNow($this->getRequest()->getUrl()->setPath('eventdb/events/details')); + } +} diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php new file mode 100644 index 0000000..d812918 --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,45 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Controllers; + +use Icinga\Module\Eventdb\Forms\Config\BackendConfigForm; +use Icinga\Module\Eventdb\Forms\Config\GlobalConfigForm; +use Icinga\Module\Eventdb\Forms\Config\MonitoringConfigForm; +use Icinga\Web\Controller; + +class ConfigController extends Controller +{ + public function init() + { + $this->assertPermission('config/modules'); + parent::init(); + } + + public function indexAction() + { + $backendConfig = new BackendConfigForm(); + $backendConfig + ->setIniConfig($this->Config()) + ->handleRequest(); + $this->view->backendConfig = $backendConfig; + + $globalConfig = new GlobalConfigForm(); + $globalConfig + ->setIniConfig($this->Config()) + ->handleRequest(); + $this->view->globalConfig = $globalConfig; + + $this->view->tabs = $this->Module()->getConfigTabs()->activate('config'); + } + + public function monitoringAction() + { + $monitoringConfig = new MonitoringConfigForm(); + $monitoringConfig + ->setIniConfig($this->Config()) + ->handleRequest(); + $this->view->form = $monitoringConfig; + $this->view->tabs = $this->Module()->getConfigTabs()->activate('monitoring'); + } +} diff --git a/application/controllers/EventController.php b/application/controllers/EventController.php new file mode 100644 index 0000000..f6a323c --- /dev/null +++ b/application/controllers/EventController.php @@ -0,0 +1,216 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Eventdb\Event; +use Icinga\Module\Eventdb\EventdbController; +use Icinga\Module\Eventdb\Forms\Event\EventCommentForm; +use Icinga\Module\Eventdb\Hook\DetailviewExtensionHook; +use Icinga\Module\Eventdb\Web\EventdbOutputFormat; +use Icinga\Web\Hook; +use Icinga\Web\Url; + +class EventController extends EventdbController +{ + public function indexAction() + { + $eventId = $this->params->getRequired('id'); + + $url = Url::fromRequest(); + + $this->getTabs()->add('event', array( + 'active' => ! $this->isFormatRequest(), + 'title' => $this->translate('Event'), + 'url' => $url->without(array('format')) + ))->extend(new EventdbOutputFormat(array(), array(EventdbOutputFormat::TYPE_TEXT))); + + $columnConfig = $this->Config('columns'); + if (! $columnConfig->isEmpty()) { + $additionalColumns = $columnConfig->keys(); + } else { + $additionalColumns = array(); + } + + $event = $this->getDb() + ->select() + ->from('event'); + + $columns = array_merge($event->getColumns(), $additionalColumns); + + $event->from('event', $columns); + $event->where('id', $eventId); + + $event->applyFilter(Filter::matchAny(array_map( + '\Icinga\Data\Filter\Filter::fromQueryString', + $this->getRestrictions('eventdb/events/filter', 'eventdb/events') + ))); + + $eventData = $event->fetchRow(); + if (! $eventData) { + throw new NotFoundError('Could not find event with id %d', $eventId); + } + + $eventObj = Event::fromData($eventData); + + $groupedEvents = null; + if ($this->getDb()->hasCorrelatorExtensions()) { + $group_leader = (int) $eventObj->group_leader; + if ($group_leader > 0) { + // redirect to group leader + $this->redirectNow(Url::fromPath('eventdb/event', array('id' => $group_leader))); + } + + if ($group_leader === -1) { + // load grouped events, if any + $groupedEvents = $this->getDb() + ->select() + ->from('event') + ->where('group_leader', $eventObj->id) + ->order('ack', 'ASC') + ->order('created', 'DESC'); + } + } + + $comments = null; + $commentForm = null; + if ($this->hasPermission('eventdb/comments')) { + $comments = $this->getDb() + ->select() + ->from('comment', array( + 'id', + 'type', + 'message', + 'created', + 'modified', + 'user' + )) + ->where('event_id', $eventId) + ->order('created', 'DESC'); + + if ($this->hasPermission('eventdb/interact')) { + $commentForm = new EventCommentForm(); + $commentForm + ->setDb($this->getDb()) + ->setFilter(Filter::expression('id', '=', $eventId)); + } + } + + $format = $this->params->get('format'); + if ($format === 'sql') { + $this->sendSqlSummary(array($event, $comments, $groupedEvents)); + } elseif ($this->isApiRequest()) { + $data = new \stdClass; + $data->event = $eventData; + if ($comments !== null) { + $data->comments = $comments; + } + if ($groupedEvents !== null) { + $data->groupedEvents = $groupedEvents; + } + $this->sendJson($data); + } elseif ($this->isTextRequest()) { + $this->view->event = $eventObj; + $this->view->columnConfig = $columnConfig; + $this->view->additionalColumns = $additionalColumns; + $this->view->groupedEvents = $groupedEvents; + $this->view->comments = $comments; + + $this->sendText(null, 'event/index-plain'); + } else { + if ($commentForm !== null) { + $commentForm->handleRequest(); + } + + $this->view->event = $eventObj; + $this->view->columnConfig = $columnConfig; + $this->view->additionalColumns = $additionalColumns; + $this->view->groupedEvents = $groupedEvents; + $this->view->comments = $comments; + $this->view->commentForm = $commentForm; + + $this->view->extensionsHtml = array(); + foreach (Hook::all('Eventdb\DetailviewExtension') as $hook) { + /** @var DetailviewExtensionHook $hook */ + $module = $this->view->escape($hook->getModule()->getName()); + $this->view->extensionsHtml[] = + '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">' + . $hook->setView($this->view)->getHtmlForEvent($eventObj) + . '</div>'; + } + } + } + + /** + * @deprecated redirects to index view now + */ + public function commentsAction() + { + $this->redirectNow( + Url::fromPath( + 'eventdb/event', + array('id' => $this->params->getRequired('id')) + ) + ); + } + + /** + * Action allowing you to be forwarded to host in Icinga monitoring + * + * **But** case insensitive! + */ + public function hostAction() + { + $host = $this->params->getRequired('host'); + + $backend = $this->monitoringBackend(); + + $query = $backend->select() + ->from('hoststatus', array('host_name')) + ->where('host', $host); + + $realHostname = $query->fetchOne(); + + if ($realHostname !== null && $realHostname !== false) { + $this->redirectNow(Url::fromPath('monitoring/host/services', array('host' => $realHostname))); + } else { + throw new NotFoundError('Could not find a hostname matching: %s', $host); + } + } + + /** + * Action allowing you to be forwarded to host in Icinga monitoring + * + * **But** case insensitive! + */ + public function serviceAction() + { + $host = $this->params->getRequired('host'); + $service = $this->params->getRequired('service'); + + $backend = $this->monitoringBackend(); + + $query = $backend->select() + ->from('servicestatus', array('host_name', 'service')) + ->where('host', $host) + ->where('service', $service); + + $realService = $query->fetchRow(); + + if ($realService !== null && $realService !== false) { + $this->redirectNow( + Url::fromPath( + 'monitoring/service/show', + array( + 'host' => $realService->host_name, + 'service' => $realService->service + ) + ) + ); + } else { + throw new NotFoundError('Could not find a service "%s" for host "%s"', $service, $host); + } + } +} diff --git a/application/controllers/EventsController.php b/application/controllers/EventsController.php new file mode 100644 index 0000000..09e5a8d --- /dev/null +++ b/application/controllers/EventsController.php @@ -0,0 +1,186 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Controllers; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Eventdb\EventdbController; +use Icinga\Module\Eventdb\Forms\Event\EventCommentForm; +use Icinga\Module\Eventdb\Forms\Events\AckFilterForm; +use Icinga\Module\Eventdb\Forms\Events\SeverityFilterForm; +use Icinga\Module\Eventdb\Hook\DetailviewExtensionHook; +use Icinga\Module\Eventdb\Web\EventdbOutputFormat; +use Icinga\Util\StringHelper; +use Icinga\Web\Hook; +use Icinga\Web\Url; + +class EventsController extends EventdbController +{ + public function init() + { + parent::init(); + $this->view->title = 'EventDB: ' . $this->translate('Events'); + } + + public function indexAction() + { + $this->assertPermission('eventdb/events'); + + $this->getTabs()->add('events', array( + 'active' => ! $this->isFormatRequest(), + 'title' => $this->translate('Events'), + 'url' => Url::fromRequest()->without(array('format')) + ))->extend(new EventdbOutputFormat(array(), array(EventdbOutputFormat::TYPE_TEXT))); + + $columnConfig = $this->Config('columns'); + if ($this->params->has('columns')) { + $additionalColumns = StringHelper::trimSplit($this->params->get('columns')); + } elseif (! $columnConfig->isEmpty()) { + $additionalColumns = $columnConfig->keys(); + } else { + $additionalColumns = array(); + } + + $events = $this->getDb()->select() + ->from('event'); + + $columns = array_merge($events->getColumns(), $additionalColumns); + $events->columns($columns); + + $events->applyFilter(Filter::matchAny(array_map( + '\Icinga\Data\Filter\Filter::fromQueryString', + $this->getRestrictions('eventdb/events/filter', 'eventdb/events') + ))); + + $this->getDb()->filterGroups($events); + + $this->setupPaginationControl($events); + + $this->setupFilterControl( + $events, + array( + 'host_name' => $this->translate('Host'), + 'host_address' => $this->translate('Host Address'), + 'type' => $this->translate('Type'), + 'program' => $this->translate('Program'), + 'facility' => $this->translate('Facility'), + 'priority' => $this->translate('Priority'), + 'message' => $this->translate('Message'), + 'ack' => $this->translate('Acknowledged'), + 'created' => $this->translate('Created') + ), + array('host_name'), + array('columns', 'format') + ); + + $this->setupLimitControl(); + + $this->setupSortControl( + array( + 'host_name' => $this->translate('Host'), + 'host_address' => $this->translate('Host Address'), + 'type' => $this->translate('Type'), + 'program' => $this->translate('Program'), + 'facility' => $this->translate('Facility'), + 'priority' => $this->translate('Priority'), + 'message' => $this->translate('Message'), + 'ack' => $this->translate('Acknowledged'), + 'created' => $this->translate('Created') + ), + $events, + array('created' => 'desc') + ); + + if ($this->view->compact) { + $events->peekAhead(); + } + + if ($this->params->get('format') === 'sql') { + $this->sendSqlSummary($events); + } elseif ($this->isApiRequest()) { + $data = new \stdClass; + $data->events = $events->fetchAll(); + $this->sendJson($data); + exit; + } elseif ($this->isTextRequest()) { + $this->view->columnConfig = $this->Config('columns'); + $this->view->additionalColumns = $additionalColumns; + $this->view->events = $events; + + $this->sendText(null, 'events/index-plain'); + } else { + $this->setAutorefreshInterval(15); + + $severityFilterForm = new SeverityFilterForm(); + $severityFilterForm->handleRequest(); + + $ackFilterForm = new AckFilterForm(); + $ackFilterForm->handleRequest(); + + $this->view->ackFilterForm = $ackFilterForm; + $this->view->columnConfig = $this->Config('columns'); + $this->view->additionalColumns = $additionalColumns; + $this->view->events = $events; + $this->view->severityFilterForm = $severityFilterForm; + } + } + + public function detailsAction() + { + $this->assertPermission('eventdb/events'); + + $url = Url::fromRequest()->without(array('format')); + + $this->getTabs()->add('events', array( + 'active' => ! $this->isFormatRequest(), + 'title' => $this->translate('Events'), + 'url' => $url + ))->extend(new EventdbOutputFormat(array(), array(EventdbOutputFormat::TYPE_TEXT))); + + $events = $this->getDb() + ->select() + ->from('event'); + + $this->getDb()->filterGroups($events); + + $filter = Filter::fromQueryString($url->getQueryString()); + $events->applyFilter($filter); + + $events->applyFilter(Filter::matchAny(array_map( + '\Icinga\Data\Filter\Filter::fromQueryString', + $this->getRestrictions('eventdb/events/filter', 'eventdb/events') + ))); + + if ($this->isApiRequest()) { + $this->sendJson($events->fetchAll()); + } elseif ($this->isTextRequest()) { + $this->view->events = $events->fetchAll(); + $this->view->columnConfig = $this->Config('columns'); + + $this->sendText(null, 'events/details-plain'); + } else { + $commentForm = null; + if ($this->hasPermission('eventdb/interact')) { + $commentForm = new EventCommentForm(); + $commentForm + ->setDb($this->getDb()) + ->setFilter($filter) + ->handleRequest(); + $this->view->commentForm = $commentForm; + } + + $this->view->events = $events->fetchAll(); + $this->view->columnConfig = $this->Config('columns'); + + $this->view->extensionsHtml = array(); + foreach (Hook::all('Eventdb\DetailviewExtension') as $hook) { + /** @var DetailviewExtensionHook $hook */ + $module = $this->view->escape($hook->getModule()->getName()); + $this->view->extensionsHtml[] = + '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">' + . $hook->setView($this->view)->getHtmlForEvents($this->view->events) + . '</div>'; + } + } + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..5322936 --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,14 @@ +<?php +/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Controllers; + +use Icinga\Web\Controller; + +class IndexController extends Controller +{ + public function indexAction() + { + $this->redirectNow('eventdb/events'); + } +} diff --git a/application/forms/Config/BackendConfigForm.php b/application/forms/Config/BackendConfigForm.php new file mode 100644 index 0000000..a9c85c1 --- /dev/null +++ b/application/forms/Config/BackendConfigForm.php @@ -0,0 +1,112 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Forms\Config; + +use Exception; +use Icinga\Data\ResourceFactory; +use Icinga\Forms\ConfigForm; + +/** + * Form for managing the connection to the EventDB backend + */ +class BackendConfigForm extends ConfigForm +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setSubmitLabel($this->translate('Save')); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $resources = array(); + foreach (ResourceFactory::getResourceConfigs() as $name => $config) { + if ($config->type === 'db') { + $resources[] = $name; + } + } + + $this->addElement( + 'select', + 'backend_resource', + array( + 'description' => $this->translate('The resource to use'), + 'label' => $this->translate('Resource'), + 'multiOptions' => array_combine($resources, $resources), + 'required' => true + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } + } + + /** + * Return whether the given values are valid + * + * @param array $formData The data to validate + * + * @return bool + */ + public function isValid($formData) + { + if (! parent::isValid($formData)) { + return false; + } + + if (($el = $this->getElement('skip_validation')) === null || ! $el->isChecked()) { + $resourceConfig = ResourceFactory::getResourceConfig($this->getValue('backend_resource')); + + if (! $this->isValidEventDbSchema($resourceConfig)) { + if ($el === null) { + $this->addSkipValidationCheckbox(); + } + + return false; + } + } + + return true; + } + + public function isValidEventDbSchema($resourceConfig) + { + try { + $db = ResourceFactory::createResource($resourceConfig); + $db->select()->from('event', array('id'))->fetchOne(); + } catch (Exception $_) { + $this->error($this->translate( + 'Cannot find the EventDB schema. Please verify that the given database ' + . 'contains the schema and that the configured user has access to it.' + )); + return false; + } + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the schema validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'description' => $this->translate( + 'Check this to not to validate the EventDB schema of the chosen resource' + ), + 'ignore' => true, + 'label' => $this->translate('Skip Validation'), + 'order' => 0 + ) + ); + } +} diff --git a/application/forms/Config/GlobalConfigForm.php b/application/forms/Config/GlobalConfigForm.php new file mode 100644 index 0000000..5b4bf2a --- /dev/null +++ b/application/forms/Config/GlobalConfigForm.php @@ -0,0 +1,32 @@ +<?php +/* Icinga Web 2 - EventDB | (c) 2018 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Forms\Config; + +use Icinga\Forms\ConfigForm; + +class GlobalConfigForm extends ConfigForm +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setSubmitLabel($this->translate('Save')); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'global_default_filter', + array( + 'description' => $this->translate('Filter to be used by the menu link for EventDB by default'), + 'label' => $this->translate('Default Filter') + ) + ); + } +} diff --git a/application/forms/Config/MonitoringConfigForm.php b/application/forms/Config/MonitoringConfigForm.php new file mode 100644 index 0000000..e1c7638 --- /dev/null +++ b/application/forms/Config/MonitoringConfigForm.php @@ -0,0 +1,72 @@ +<?php +/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Forms\Config; + +use Icinga\Forms\ConfigForm; + +/** + * Form for managing settings for the integration into monitoring module + */ +class MonitoringConfigForm extends ConfigForm +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setSubmitLabel($this->translate('Save')); + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'monitoring_custom_var', + array( + 'description' => $this->translate('Name of the custom variable to enable EventDB integration for (usually "edb")'), + 'label' => $this->translate('Custom Variable') + ) + ); + + $this->addElement( + 'checkbox', + 'monitoring_always_on_host', + array( + 'description' => $this->translate('Always enable the integration on hosts, even when the custom variable is not set'), + 'label' => $this->translate('Always enable for hosts') + ) + ); + + $this->addElement( + 'checkbox', + 'monitoring_always_on_service', + array( + 'description' => $this->translate('Always enable the integration on services, even when the custom variable is not set'), + 'label' => $this->translate('Always enable for services') + ) + ); + + $this->addElement( + 'checkbox', + 'monitoring_detailview_disable', + array( + 'description' => $this->translate('Disable the detail view inside the monitoring module'), + 'label' => $this->translate('Disable the detail view') + ) + ); + + $this->addElement( + 'text', + 'monitoring_detailview_filter', + array( + 'description' => $this->translate('Filter events in the detail view area inside the monitoring module'), + 'label' => $this->translate('Filter the detail view'), + 'value' => 'ack=0', // Also see DetailviewExtension + ) + ); + } +} diff --git a/application/forms/Event/EventCommentForm.php b/application/forms/Event/EventCommentForm.php new file mode 100644 index 0000000..130910c --- /dev/null +++ b/application/forms/Event/EventCommentForm.php @@ -0,0 +1,144 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Forms\Event; + +use Exception; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Eventdb\Eventdb; +use Icinga\Web\Form; + +/** + * Form for managing the connection to the EventDB backend + */ +class EventCommentForm extends Form +{ + /** + * @var Eventdb + */ + protected $db; + + protected $filter; + + protected static $types = array( + 'comment', + 'ack', + 'revoke' + ); + + /** + * {@inheritdoc} + */ + public static $defaultElementDecorators = array( + array('ViewHelper', array('separator' => '')), + array('Errors', array('separator' => '')), + ); + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setSubmitLabel($this->translate('Submit')); + } + + public function setDb(Eventdb $db) + { + $this->db = $db; + return $this; + } + + public function setFilter(Filter $filter) + { + $this->filter = $filter; + return $this; + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $this->addElement( + 'select', + 'type', + array( + 'label' => $this->translate('Type'), + 'multiOptions' => array( + 0 => $this->translate('Comment'), + 1 => $this->translate('Acknowledge'), + 2 => $this->translate('Revoke') + ), + 'required' => true, + 'value' => 1, + ) + ); + $this->addElement( + 'text', + 'comment', + array( + 'label' => $this->translate('Comment'), + 'required' => true + ) + ); + } + + public function onSuccess() + { + $type = $this->getValue('type'); + $comment = $this->getValue('comment'); + $username = $this->Auth()->getUser()->getUsername(); + + $events = $this->db->select()->from('event', array('id'))->applyFilter($this->filter); + + $dbAdapter = $this->db->getDataSource()->getDbAdapter(); + + $dbAdapter->beginTransaction(); + try { + foreach ($events as $event) { + $this->db->insert('comment', array( + 'event_id' => $event->id, + 'type' => $type, + 'message' => $comment, + 'created' => date(Eventdb::DATETIME_FORMAT), + 'modified' => date(Eventdb::DATETIME_FORMAT), + 'user' => $username + )); + + if ($type !== '0') { + $ackFilter = Filter::expression('id', '=', $event->id); + if ($this->db->hasCorrelatorExtensions()) { + $ackFilter = Filter::matchAny($ackFilter, Filter::where('group_leader', $event->id)); + } + $this->db->update('event', array( + 'ack' => $type === '1' ? 1 : 0 + ), $ackFilter); + } + } + $dbAdapter->commit(); + return true; + } catch (Exception $e) { + $dbAdapter->rollback(); + $this->error($e->getMessage()); + return false; + } + } + + /** + * {@inheritdoc} + */ + public function loadDefaultDecorators() + { + parent::loadDefaultDecorators(); + + $this->removeDecorator('FormHints'); + } + + public function addSubmitButton() + { + parent::addSubmitButton(); + + $btn = $this->getElement('btn_submit'); + $btn->removeDecorator('HtmlTag'); + } +} diff --git a/application/forms/Events/AckFilterForm.php b/application/forms/Events/AckFilterForm.php new file mode 100644 index 0000000..829c6ce --- /dev/null +++ b/application/forms/Events/AckFilterForm.php @@ -0,0 +1,80 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Forms\Events; + +use Icinga\Data\Filter\Filter; +use Icinga\Web\Form; + +class AckFilterForm extends Form +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline ack-filter-form'); + } + + /** + * {@inheritdoc} + */ + public function addSubmitButton() + { + if ((bool) $this->getRequest()->getUrl()->getParams()->get('ack', true)) { + $icon = 'ok'; + $title = $this->translate('Hide acknowledged events'); + } else { + $icon = 'cancel'; + $title = $this->translate('Show also acknowledged events'); + } + + $this->addElements(array( + array( + 'button', + 'btn_submit', + array( + 'class' => 'link-button spinner', + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ), + 'escape' => false, + 'ignore' => true, + 'label' => $this->getView()->icon($icon) . $this->translate('Ack'), + 'type' => 'submit', + 'title' => $title, + 'value' => $this->translate('Ack') + ) + ) + )); + + return $this; + } + + public function onSuccess() + { + $redirect = clone $this->getRequest()->getUrl(); + $params = $redirect->getParams(); + $modifyFilter = $params->shift('modifyFilter'); + $columns = $params->shift('columns'); + if (! (bool) $this->getRequest()->getUrl()->getParams()->get('ack', true)) { + $params->remove('ack'); + } else { + $redirect->setQueryString( + Filter::fromQueryString($redirect->getQueryString()) + ->andFilter(Filter::expression('ack', '=', 0)) + ->toQueryString() + ); + } + $params = $redirect->getParams(); + if ($modifyFilter) { + $params->add('modifyFilter'); + } + if ($columns) { + $params->add('columns', $columns); + } + $this->setRedirectUrl($redirect); + return true; + } +} diff --git a/application/forms/Events/SeverityFilterForm.php b/application/forms/Events/SeverityFilterForm.php new file mode 100644 index 0000000..bd1ec5e --- /dev/null +++ b/application/forms/Events/SeverityFilterForm.php @@ -0,0 +1,224 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Eventdb\Forms\Events; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterAnd; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Data\Filter\FilterMatch; +use Icinga\Module\Eventdb\Event; +use Icinga\Web\Form; + +class SeverityFilterForm extends Form +{ + protected $includedPriorities; + protected $excludedPriorities; + + /** + * @var FilterChain + */ + protected $filter; + + /** + * @var array + */ + protected $filterEditorParams = array('modifyFilter', 'addFilter'); + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setAttrib('class', 'inline severity-filter-form'); + } + + protected function findPriorities(Filter $filter, $sign, &$target) + { + if ($filter->isEmpty()) { + return; + } + + if ($filter->isChain()) { + /** @var FilterChain $filter */ + foreach ($filter->filters() as $part) { + /** @var Filter $part */ + if (! $part->isEmpty() && $part->isExpression()) { + /** @var FilterMatch $part */ + if (strtolower($part->getColumn()) === 'priority' && $part->getSign() === $sign) { + $expression = $part->getExpression(); + if (is_array($expression)) { + foreach ($expression as $priority) { + $target[(int) $priority] = $part; + } + } else { + $target[(int) $expression] = $part; + } + } + } else { + /** @var FilterChain $part */ + foreach ($part->filters() as $or) { + /** @var FilterExpression $or */ + if (strtolower($or->getColumn()) === 'priority' && $or->getSign() === $sign) { + $expression = $or->getExpression(); + if (is_array($expression)) { + foreach ($expression as $priority) { + $target[(int) $priority] = $or; + } + } else { + $target[(int) $expression] = $or; + } + } + } + } + } + } else { + /** @var FilterMatch $filter */ + if (strtolower($filter->getColumn()) === 'priority' && $filter->getSign() === $sign) { + $expression = $filter->getExpression(); + if (is_array($expression)) { + foreach ($expression as $priority) { + $target[(int) $priority] = $filter; + } + } else { + $target[(int) $expression] = $filter; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function createElements(array $formData) + { + $includedPriorities = array(); + $excludedPriorities = array(); + $params = $this->getRequest()->getUrl()->getParams() + ->without($this->filterEditorParams) + ->without('columns') + ->without('page'); + + $filter = Filter::fromQueryString((string) $params); + + $this->findPriorities($filter, '=', $includedPriorities); + $this->findPriorities($filter, '!=', $excludedPriorities); + + foreach (Event::$priorities as $id => $priority) { + $class = $priority; + if ( + (empty($includedPriorities) or isset($includedPriorities[$id])) + && ! isset($excludedPriorities[$id]) + ) { + $class .= ' active'; + $title = $this->translate('Filter out %s'); + } else { + $title = $this->translate('Filter in %s'); + } + + $label = ucfirst(substr($priority, 0, 1)); + if ($id === 3) { + $label .= substr($priority, 1, 1); + } + $this->addElement( + 'submit', + $priority, + array( + 'class' => $class, + 'label' => $label, + 'title' => sprintf($title, ucfirst($priority)), + ) + ); + } + + $this->includedPriorities = $includedPriorities; + $this->excludedPriorities = $excludedPriorities; + + $this->filter = $filter; + } + + public function onSuccess() + { + $postData = $this->getRequest()->getPost(); + unset($postData['formUID']); + unset($postData['CSRFToken']); + reset($postData); + $priority = Event::getPriorityId(key($postData)); + $redirect = clone $this->getRequest()->getUrl(); + + // convert inclusion to exclusion + if (! empty($this->includedPriorities)) { + if (empty($this->excludedPriorities)) { + $this->excludedPriorities = array(); + } + // set exclusion with for all not included values + foreach (array_keys(Event::$priorities) as $id) { + if (! isset($this->includedPriorities[$id]) + && ! isset($this->excludedPriorities[$id]) + ) { + $this->excludedPriorities[$id] = true; + } + } + // purge from inclusions from filter + if ($this->filter instanceof FilterChain) { + foreach ($this->includedPriorities as $filter) { + if ($filter instanceof Filter) { + /** @var Filter $filter */ + $this->filter = $this->filter->removeId($filter->getId()); + } + } + } + } + + if ($this->filter instanceof FilterChain) { + // purge existing exclusions from a complex filter + foreach ($this->excludedPriorities as $filter) { + if ($filter instanceof Filter) { + /** @var Filter $filter */ + $this->filter = $this->filter->removeId($filter->getId()); + } + } + } elseif (! empty($this->excludedPriorities)) { + // empty the filter - because it only was a simple exclusion + $this->filter = new FilterAnd; + } + + // toggle exclusion + if (isset($this->excludedPriorities[$priority])) { + // in exclusion: just remove + unset($this->excludedPriorities[$priority]); + } else { + // not set: add to exclusion + $this->excludedPriorities[$priority] = true; + } + + $priorityFilter = Filter::matchAll(); + foreach (array_keys($this->excludedPriorities) as $id) { + $priorityFilter->andFilter(Filter::expression('priority', '!=', $id)); + } + + if ($this->filter->isEmpty()) { + // set the Filter + $this->filter = $priorityFilter; + } else { + // append our filter to the rest of the existing Filter + $this->filter = $this->filter->andFilter($priorityFilter); + } + + $redirect->setQueryString($this->filter->toQueryString()); + + $requestParams = $this->getRequest()->getUrl()->getParams(); + $redirectParams = $redirect->getParams(); + foreach ($this->filterEditorParams as $filterEditorParam) { + if ($requestParams->has($filterEditorParam)) { + $redirectParams->add($filterEditorParam); + } + } + if ($requestParams->has('columns')) { + $redirectParams->add('columns', $this->getRequest()->getUrl()->getParam('columns')); + } + $this->setRedirectUrl($redirect); + return true; + } +} diff --git a/application/locale/de_DE/LC_MESSAGES/eventdb.mo b/application/locale/de_DE/LC_MESSAGES/eventdb.mo Binary files differnew file mode 100644 index 0000000..0bcec3c --- /dev/null +++ b/application/locale/de_DE/LC_MESSAGES/eventdb.mo diff --git a/application/locale/de_DE/LC_MESSAGES/eventdb.po b/application/locale/de_DE/LC_MESSAGES/eventdb.po new file mode 100644 index 0000000..29b432a --- /dev/null +++ b/application/locale/de_DE/LC_MESSAGES/eventdb.po @@ -0,0 +1,413 @@ +# Icinga Web 2 - Head for multiple monitoring backends. +# Copyright (C) 2018 Icinga Development Team +# This file is distributed under the same license as Eventdb Module. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Eventdb Module (1.1.0)\n" +"Report-Msgid-Bugs-To: dev@icinga.com\n" +"POT-Creation-Date: 2018-01-25 14:31+0000\n" +"PO-Revision-Date: 2018-01-25 15:32+0100\n" +"Last-Translator: Markus Frosch <markus.frosch@icinga.com>\n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Basepath: .\n" +"Language-Team: \n" +"X-Generator: Poedit 2.0.5\n" +"X-Poedit-SearchPath-0: .\n" + +#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:15 +#, php-format +msgctxt "Multi-selection count" +msgid "%s row(s) selected" +msgstr "%s Zeilen ausgewählt" + +#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:44 +#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:47 +msgid "Ack" +msgstr "Bestätigt" + +#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:69 +msgid "Acknowledge" +msgstr "Bestätigen" + +#: ../../../../modules/eventdb/application/views/helpers/Column.php:23 +#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:10 +#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:9 +#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:24 +#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:72 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:18 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:51 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:27 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:161 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:69 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:87 +msgid "Acknowledged" +msgstr "Bestätigt" + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:9 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:9 +msgid "Acknowledgement" +msgstr "Bestätigung" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:60 +msgid "Actions" +msgstr "Aktionen" + +#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:41 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:118 +msgid "Add comment / acknowledge" +msgstr "Kommentar / Bestätigung hinzufügen" + +#: ../../../../modules/eventdb/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php:151 +msgid "All events for host" +msgstr "Alle Ereignisse für Host" + +#: ../../../../modules/eventdb/configuration.php:40 +msgid "Allow to acknowledge and comment events" +msgstr "Erlauben Ereignisse zu Bestätigen und zu Kommentieren" + +#: ../../../../modules/eventdb/configuration.php:35 +msgid "Allow to view comments" +msgstr "Erlauben Kommentare zu sehen" + +#: ../../../../modules/eventdb/configuration.php:30 +msgid "Allow to view events" +msgstr "Erlauben Ereignisse zu sehen" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:40 +msgid "Always enable for hosts" +msgstr "Immer für Hosts aktivieren" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:49 +msgid "Always enable for services" +msgstr "Immer für Services aktivieren" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:39 +msgid "" +"Always enable the integration on hosts, even when the custom variable is not " +"set" +msgstr "" +"Immer bei Interaktionen mit Hosts anzeigen, auch wenn die angepasste " +"Variable nicht gesetzt ist" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:48 +msgid "" +"Always enable the integration on services, even when the custom variable is " +"not set" +msgstr "" +"Immer bei Interaktionen mit Services anzeigen, auch wenn die angepasste " +"Variable nicht gesetzt ist" + +#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:44 +msgid "At least one event is set to auto-clear." +msgstr "Mindestens ein Ereignis ist für automatische Bestätigung gesetzt." + +#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:25 +#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:73 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:28 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:162 +msgid "Auto-Clear" +msgstr "Automatische Bestätigung" + +#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:86 +msgid "" +"Cannot find the EventDB schema. Please verify that the given database " +"contains the schema and that the configured user has access to it." +msgstr "" +"Kann das EventDB-Schema nicht finden. Bitte stellen Sie sicher, dass die " +"angegebene Datenbankverbindung das Schema enthält und dass der konfigurierte " +"Benutzer Zugriff darauf hat." + +#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:104 +msgid "Check this to not to validate the EventDB schema of the chosen resource" +msgstr "" +"Häkchen setzen um das EventDB Schema der gewählten Ressource nicht zu " +"überprüfen." + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:8 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:8 +#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:68 +#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:80 +msgid "Comment" +msgstr "Kommentar" + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:28 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:128 +msgid "Comments" +msgstr "Kommentare" + +#: ../../../../modules/eventdb/configuration.php:19 +msgid "Config" +msgstr "Konfiguration" + +#: ../../../../modules/eventdb/configuration.php:18 +#, fuzzy +msgid "Configure EventDB" +msgstr "EventDB Datenbankverbindung einrichten" + +#: ../../../../modules/eventdb/configuration.php:23 +#: ../../../../modules/eventdb/application/views/scripts/config/monitoring.phtml:5 +msgid "Configure integration into the monitoring module" +msgstr "Interaktionen im Monitoring Modul" + +#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:19 +msgid "Configure module" +msgstr "Modul konfigurieren" + +#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:7 +msgid "Create a New Resource" +msgstr "Neue Ressource erstellen" + +#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:14 +msgid "Create a new resource" +msgstr "Neue Ressource erstellen" + +#: ../../../../modules/eventdb/application/controllers/EventsController.php:70 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:88 +msgid "Created" +msgstr "Erstellt" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:31 +msgid "Custom Variable" +msgstr "Angepasste Variable" + +#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:5 +#, fuzzy +msgid "Database backend" +msgstr "EventDB Datenbankverbindung einrichten" + +#: ../../../../modules/eventdb/application/forms/Config/GlobalConfigForm.php:28 +msgid "Default Filter" +msgstr "Standard Filter" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:36 +msgid "Details" +msgstr "Details" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:58 +msgid "Disable the detail view" +msgstr "Detailansicht deaktivieren" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:57 +msgid "Disable the detail view inside the monitoring module" +msgstr "Die Detailansicht innerhalb des Monitoring Moduls deaktivieren" + +#: ../../../../modules/eventdb/application/controllers/EventController.php:26 +msgid "Event" +msgstr "Ereignis" + +#: ../../../../modules/eventdb/application/controllers/EventsController.php:22 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:31 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:136 +msgid "Events" +msgstr "Ereignisse" + +# Spezifische Syslog Bezeichnung +#: ../../../../modules/eventdb/application/controllers/EventsController.php:66 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:84 +msgid "Facility" +msgstr "Facility" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:66 +msgid "Filter events in the detail view area inside the monitoring module" +msgstr "Ereignisse der Detailansicht im Monitoring Modul filtern" + +#: ../../../../modules/eventdb/application/forms/Events/SeverityFilterForm.php:117 +#, php-format +msgid "Filter in %s" +msgstr "Filtern nach %s" + +#: ../../../../modules/eventdb/application/forms/Events/SeverityFilterForm.php:115 +#, php-format +msgid "Filter out %s" +msgstr "Filtern ohne %s" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:67 +msgid "Filter the detail view" +msgstr "Detailansicht filtern" + +#: ../../../../modules/eventdb/application/forms/Config/GlobalConfigForm.php:27 +msgid "Filter to be used by the menu link for EventDB by default" +msgstr "Filter für die URL des Menü Eintrages der EventDB" + +#: ../../../../modules/eventdb/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php:134 +msgid "Filtered events" +msgstr "Gefilterte Ereignisse" + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:42 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:147 +msgid "Grouped Events" +msgstr "Gruppierte Ereignisse" + +#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:26 +msgid "Hide acknowledged events" +msgstr "Bestätigte Ereignisse verstecken" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:65 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:62 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:80 +msgid "Host" +msgstr "Host" + +#: ../../../../modules/eventdb/application/controllers/EventsController.php:63 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:81 +msgid "Host Address" +msgstr "Host Adresse" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:105 +msgid "Host service status" +msgstr "Host Service Status" + +#: ../../../../modules/eventdb/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php:66 +msgid "Loading" +msgstr "Lade" + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:35 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:50 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:68 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:86 +msgid "Message" +msgstr "Meldung" + +#: ../../../../modules/eventdb/configuration.php:24 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:102 +msgid "Monitoring" +msgstr "Monitoring" + +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:30 +msgid "" +"Name of the custom variable to enable EventDB integration for (usually \"edb" +"\")" +msgstr "" +"Name der angepassten Variable um die EventDB Integration zu aktivieren " +"(normalerweise \"edb\")" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:130 +msgid "No comments recorded for this event yet." +msgstr "Keine Kommentare für dieses Ereignis." + +#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:3 +#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:33 +msgid "No events recorded yet." +msgstr "Keine Ereignisse gespeichert." + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:63 +msgid "Other events for" +msgstr "Andere Ereignisse für" + +#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:45 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:122 +msgid "Please only acknowledge manually, if you know what that means." +msgstr "Bitte nur manuell Bestätigen wenn Sie wissen was das bedeutet." + +#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:6 +msgctxt "Multi-selection help" +msgid "" +"Press and hold the Ctrl key while clicking on rows to select multiple rows " +"or press and hold the Shift key to select a range of rows" +msgstr "" +"Drücken und halten Sie STRG während Sie auf Spalten klicken, um mehrere " +"auszuwählen. Mit der Umschalttaste können Sie Bereiche von Spalten auswählen" + +#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:9 +#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:8 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:17 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:50 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:67 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:85 +msgid "Priority" +msgstr "Priorität" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:75 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:65 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:83 +msgid "Program" +msgstr "Programm" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:86 +msgid "Program and Host" +msgstr "Programm und Host" + +#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:40 +msgid "Resource" +msgstr "Ressource" + +#: ../../../../modules/eventdb/configuration.php:45 +msgid "Restrict views to the events that match the filter" +msgstr "Ereignisse mit einem Filter einschränken" + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:10 +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:10 +msgid "Revocation" +msgstr "Widerruf" + +#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:70 +msgid "Revoke" +msgstr "Wiederrufen" + +#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:20 +#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:18 +#: ../../../../modules/eventdb/application/forms/Config/GlobalConfigForm.php:15 +msgid "Save" +msgstr "Speichern" + +#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:84 +msgid "Show More" +msgstr "Mehr zeigen" + +#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:29 +msgid "Show also acknowledged events" +msgstr "Auch bestätigte Ereignisse anzeigen" + +#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:107 +msgid "Skip Validation" +msgstr "Überprüfung überspringen" + +#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:42 +msgid "Submit" +msgstr "Absenden" + +#: ../../../../modules/eventdb/library/Eventdb/Web/EventdbOutputFormat.php:49 +msgid "Text" +msgstr "Text" + +#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:39 +msgid "The resource to use" +msgstr "Die zu benutzenden Ressource" + +#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:121 +msgid "This event is set to auto-clear." +msgstr "Das Ereignis ist auf automatische Bestätigung gesetzt." + +#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:7 +#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:6 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:15 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:48 +msgid "Timestamp" +msgstr "Zeitstempel" + +#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:12 +#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:11 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:20 +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:53 +#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:66 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:64 +#: ../../../../modules/eventdb/application/controllers/EventsController.php:82 +msgid "Type" +msgstr "Typ" + +#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:33 +msgid "User" +msgstr "Benutzername" + +#: ../../../../modules/eventdb/library/Eventdb/Eventdb.php:141 +msgid "You need to configure a resource to access the EventDB database first" +msgstr "Sie müssen zuerst die Datenbank der EventDB konfigurieren" + +#~ msgid "Backend" +#~ msgstr "Datenbank" diff --git a/application/views/helpers/Column.php b/application/views/helpers/Column.php new file mode 100644 index 0000000..b343de4 --- /dev/null +++ b/application/views/helpers/Column.php @@ -0,0 +1,51 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +use Icinga\Module\Eventdb\Event; + +class Zend_View_Helper_Column extends Zend_View_Helper_Abstract +{ + public function column($column, Event $event, $classes = array()) + { + switch ($column) { + case 'host_name': + $default = 'host_url'; + break; + case 'message': + $default = 'message'; + break; + default: + $default = null; + break; + } + + if ($column === 'ack') { + $html = $event->$column ? $this->view->icon('ok', $this->view->translate('Acknowledged')) : '-'; + } else { + $renderer = $this->view->columnConfig->get($column, 'renderer', $default); + + switch ($renderer) { + case 'host_url': + $html = $this->view->qlink($event->$column, 'eventdb/event/host', + array('host' => $event->$column)); + break; + case 'service_url': + $html = $this->view->qlink($event->$column, 'eventdb/event/service', + array('service' => $event->$column, 'host' => $event->host_name)); + break; + case 'url': + $html = $this->view->qlink($event->$column, $event->$column); + break; + case 'message': + $html = $this->view->eventMessage($event->$column); + break; + default: + $html = $this->view->escape($event->$column); + break; + } + } + + return '<td class="' . 'event-' . $this->view->escape($column) . ' ' + . implode(' ', $classes) . '" data-base-target="_next">' . $html . '</td>'; + } +} diff --git a/application/views/helpers/ColumnHeader.php b/application/views/helpers/ColumnHeader.php new file mode 100644 index 0000000..3c7f7cd --- /dev/null +++ b/application/views/helpers/ColumnHeader.php @@ -0,0 +1,17 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +class Zend_View_Helper_ColumnHeader extends Zend_View_Helper_Abstract +{ + public function columnHeader($columnHeader, $classes = array(), $plain = false) + { + $header = $this->view->columnConfig->get($columnHeader, 'label', ucwords(str_replace('_', ' ', $columnHeader))); + if ($plain) { + return $header; + } + $htm = '<th classes="' . implode(' ', $classes) . '">'; + $htm .= $this->view->escape($header); + $htm .= '</th>'; + return $htm; + } +} diff --git a/application/views/helpers/Event.php b/application/views/helpers/Event.php new file mode 100644 index 0000000..f61a793 --- /dev/null +++ b/application/views/helpers/Event.php @@ -0,0 +1,12 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +use Icinga\Module\Eventdb\Event; + +class Zend_View_Helper_Event extends Zend_View_Helper_Abstract +{ + public function event($data) + { + return Event::fromData($data); + } +} diff --git a/application/views/helpers/EventMessage.php b/application/views/helpers/EventMessage.php new file mode 100644 index 0000000..de52518 --- /dev/null +++ b/application/views/helpers/EventMessage.php @@ -0,0 +1,70 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +use Icinga\Application\Config; + +class Zend_View_Helper_EventMessage extends Zend_View_Helper_Abstract +{ + /** + * The RegExp for locating URLs. + * + * Modifications: + * - Don't allow ; in + * + * @source https://mathiasbynens.be/demo/url-regex + */ + const URL_REGEX = '@(https?)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s;]*)?@i'; + + /** + * Purifier instance + * + * @var HTMLPurifier + */ + protected static $purifier; + + public function eventMessage($message) + { + $htm = $this->getPurifier()->purify($message); + + // search for URLs and make them a link + $htm = preg_replace_callback( + static::URL_REGEX, + function ($match) { + return sprintf( + '<a href="%s" target="_blank">%s</a>', + htmlspecialchars($match[0]), + htmlspecialchars($match[0]) + ); + }, + $htm + ); + + return $htm; + } + + /** + * Get the purifier instance + * + * @return HTMLPurifier + */ + protected function getPurifier() + { + if (self::$purifier === null) { + require_once 'HTMLPurifier/Bootstrap.php'; + require_once 'HTMLPurifier.php'; + require_once 'HTMLPurifier.autoload.php'; + + $config = HTMLPurifier_Config::createDefault(); + $config->set('Core.EscapeNonASCIICharacters', true); + $config->set('HTML.Allowed', Config::module('eventdb')->get( + 'frontend', + 'allowed_html', + 'p,br,b,a[href|target],i,table,tr,td[colspan],div,*[class]' + )); + $config->set('Attr.AllowedFrameTargets', array('_blank')); + $config->set('Cache.DefinitionImpl', null); + self::$purifier = new HTMLPurifier($config); + } + return self::$purifier; + } +} diff --git a/application/views/scripts/config/index.phtml b/application/views/scripts/config/index.phtml new file mode 100644 index 0000000..2c47663 --- /dev/null +++ b/application/views/scripts/config/index.phtml @@ -0,0 +1,21 @@ +<div class="controls"> + <?= $tabs ?> +</div> +<div class="content"> + <h2><?= $this->translate('Database backend') ?></h2> + <?= $this->qlink( + $this->translate('Create a New Resource'), + 'config/createresource', + null, + array( + 'class' => 'button-link', + 'data-base-target' => '_next', + 'icon' => 'plus', + 'title' => $this->translate('Create a new resource'), + ) + ) ?> + <?= $backendConfig ?> + + <h2><?= $this->translate('Configure module') ?></h2> + <?= $globalConfig ?> +</div> diff --git a/application/views/scripts/config/monitoring.phtml b/application/views/scripts/config/monitoring.phtml new file mode 100644 index 0000000..1f6bbde --- /dev/null +++ b/application/views/scripts/config/monitoring.phtml @@ -0,0 +1,7 @@ +<div class="controls"> + <?= $tabs ?> +</div> +<div class="content"> + <h2><?= $this->translate('Configure integration into the monitoring module') ?></h2> + <?= $form ?> +</div> diff --git a/application/views/scripts/event/index-plain.phtml b/application/views/scripts/event/index-plain.phtml new file mode 100644 index 0000000..5b6ea8d --- /dev/null +++ b/application/views/scripts/event/index-plain.phtml @@ -0,0 +1,62 @@ +<?php +/** @var \Icinga\Module\Eventdb\Event $event */ +/** @var array $additionalColumns */ +/** @var \Icinga\Repository\RepositoryQuery $comments */ +/** @var \Icinga\Repository\RepositoryQuery $groupedEvents */ + +$commentTypes = array( + $this->translate('Comment'), + $this->translate('Acknowledgement'), + $this->translate('Revocation'), +); + +$displayColumns = array_merge(array('program', 'message', 'facility'), $additionalColumns); +?> +<?= $this->translate('Timestamp') ?>: <?= $event->created ?> + +<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?> +<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?> + +<?= $this->translate('Type') ?>: <?= $event->getType() ?> + +<?php foreach ($displayColumns as $col): ?> +<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?> + +<?php endforeach ?> + +<?php if ($comments->hasResult()): ?> +[ <?= $this->translate('Comments') ?> ] + +<?php foreach ($comments as $comment): ?> +<?= $commentTypes[$comment->type] ?>: <?= $comment->created ?> + +<?= $this->translate('User') ?>: <?= $comment->user ?> + +<?= $this->translate('Message') ?>: <?= $comment->message ?> + + +<?php endforeach ?> + +<?php endif; ?> +<?php if ($groupedEvents !== null && $groupedEvents->hasResult()): ?> +[ <?= $this->translate('Grouped Events') ?> ] + +<?php foreach ($groupedEvents as $groupedEventData): + /** @var \Icinga\Module\Eventdb\Event $groupedEvent */ + $groupedEvent = $this->event($groupedEventData); +?> +<?= $this->translate('Timestamp') ?>: <?= $event->created ?> + +<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?> +<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?> + +<?= $this->translate('Type') ?>: <?= $event->getType() ?> + +<?php foreach (array('host_name', 'program', 'message') as $col): ?> +<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?> + +<?php endforeach ?> + +<?php endforeach ?> +<?php endif; ?> + diff --git a/application/views/scripts/event/index.phtml b/application/views/scripts/event/index.phtml new file mode 100644 index 0000000..83dcf01 --- /dev/null +++ b/application/views/scripts/event/index.phtml @@ -0,0 +1,174 @@ +<?php +/** @var \Icinga\Module\Eventdb\Event $event */ +/** @var array $additionalColumns */ +/** @var \Icinga\Repository\RepositoryQuery $comments */ +/** @var \Icinga\Repository\RepositoryQuery $groupedEvents */ + +$commentIcons = array( + $this->icon('comment', $this->translate('Comment')), + $this->icon('ok', $this->translate('Acknowledgement')), + $this->icon('cancel', $this->translate('Revocation')) +); + +if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> +</div> +<?php endif ?> +<div class="content"> + <table class="event-summary-table"> + <tr> + <td rowspan="2" class="priority-col <?= $event->getPriority() ?> <?= $event->ack ? 'ack' : '' ?>"> + <div class="priority-label"><?= strtoupper($event->getPriority()) ?></div> + <div class="event-meta"><span class="timeago" title="<?= $event->created ?>"><?= $this->timeAgo(strtotime($event->created)) ?></span></div> + </td> + <td rowspan="2" class="icon-col"> + <?= $this->icon($event->getTypeIcon(), $event->getType()) ?> + <?php if ($event->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?> + <?php if ($event->group_autoclear) { echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?> + </td> + <?= $this->column('host_name', $event, array('selectable')) ?> + </tr> + <tr> + <?= $this->column('host_address', $event, array('selectable')) ?> + </tr> + </table> + <h2><?= $this->translate('Details') ?></h2> + <table class="name-value-table"> + <?php + $displayColumns = array_merge(array('program', 'message', 'facility'), $additionalColumns); + foreach ($displayColumns as $column): + if ($column === 'message') continue; + ?> + <tr> + <?= $this->columnHeader($column) ?> + <?= $this->column($column, $event) ?> + </tr> + <?php endforeach ?> + </table> + <?php if ($event->message): ?> + <h2><?= $this->translate('Message') ?></h2> + <div class="event-message detail preformatted"><?= $this->eventMessage($event->message) ?></div> + <?php endif; ?> + + <?php + foreach ($extensionsHtml as $extensionHtml) { + echo $extensionHtml; + } + ?> + + <h2><?= $this->translate('Actions') ?></h2> + <table class="name-value-table" data-base-target="_next"> + <tr> + <th><?= $this->translate('Other events for') ?></th> + <td><?= $this->qlink( + $this->translate('Host'), + 'eventdb/events', + array('host_name' => $event->host_name), + array( + 'icon' => 'search', + 'class' => 'action-link' + ) + ) ?> + <?php if ($event->program): ?> + <?= $this->qlink( + $this->translate('Program'), + 'eventdb/events', + array( + 'program' => $event->program, + ), + array( + 'icon' => 'search', + 'class' => 'action-link' + ) + ) ?> + <?= $this->qlink( + $this->translate('Program and Host'), + 'eventdb/events', + array( + 'host_name' => $event->host_name, + 'program' => $event->program, + ), + array( + 'icon' => 'search', + 'class' => 'action-link' + ) + ) ?> + <?php endif; ?> + </td> + </tr> + <tr> + <tr> + <th><?= $this->translate('Monitoring') ?></th> + <td> + <?= $this->qlink( + $this->translate('Host service status'), + 'eventdb/event/host', + array('host' => $event->host_name), + array( + 'icon' => 'search', + 'class' => 'action-link' + ) + ) ?> + </td> + </tr> + </table> + + <?php if (isset($commentForm)): ?> + <h2><?= $this->translate('Add comment / acknowledge') ?></h2> + <?php if ($event->group_autoclear): ?> + <div class="warning"> + <?= $this->translate('This event is set to auto-clear.') ?> + <?= $this->translate('Please only acknowledge manually, if you know what that means.') ?> + </div> + <?php endif; ?> + <div class="comment-form"><?= $commentForm ?></div> + <?php endif ?> + + <h2><?= $this->translate('Comments') ?></h2> + <?php if (! $comments->hasResult()): ?> + <p><?= $this->translate('No comments recorded for this event yet.') ?></p> + <?php else: ?> + <table class="common-table comments-table"> + <tbody> + <?php foreach ($comments as $comment): ?> + <tr> + <td class="comment-created timeago" title="<?= $comment->created ?>"><?= $this->timeAgo(strtotime($comment->created)) ?></td> + <td class="comment-type"><?= $commentIcons[$comment->type] ?></td> + <td class="comment-user"><?= $this->escape($comment->user) ?></td> + <td class="comment-message"><?= $this->escape($comment->message) ?></td> + </tr> + <?php endforeach ?> + </tbody> + </table> + <?php endif; ?> + + <?php if ($groupedEvents !== null && $groupedEvents->hasResult()): ?> + <h2><?= $this->translate('Grouped Events') ?></h2> + <table class="common-table events-table" data-base-target="_next"> + <tbody> + <?php foreach ($groupedEvents as $groupedEventData): + /** @var \Icinga\Module\Eventdb\Event $groupedEvent */ + $groupedEvent = $this->event($groupedEventData); + ?> + <tr> + <td class="priority-col <?= $groupedEvent->getPriority() ?> <?= $groupedEvent->ack ? 'ack' : '' ?>"> + <div class="priority-label"><?= strtoupper($groupedEvent->getPriority()) ?></div> + <div class="event-meta"><span class="timeago" title="<?= $groupedEvent->created ?>"><?= $this->timeAgo(strtotime($groupedEvent->created)) ?></span></div> + </td> + <td class="icon-col"> + <?= $this->icon($groupedEvent->getTypeIcon(), $groupedEvent->getType()) ?> + <?php if ($groupedEvent->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?> + <?php if ($groupedEvent->group_autoclear) { $autoClear = true; echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?> + </td> + <?= $this->column('host_name', $groupedEvent) ?> + <?= $this->column('program', $groupedEvent) ?> + <?= $this->column('message', $groupedEvent) ?> + </tr> + <?php endforeach ?> + </tbody> + </table> + <?php endif; ?> + +</div> + diff --git a/application/views/scripts/events/details-plain.phtml b/application/views/scripts/events/details-plain.phtml new file mode 100644 index 0000000..d5eb724 --- /dev/null +++ b/application/views/scripts/events/details-plain.phtml @@ -0,0 +1,18 @@ +<?php foreach($events as $eventData): + /** @var \Icinga\Module\Eventdb\Event $event */ + $event = $this->event($eventData); + $url = $this->url('eventdb/event', array('id' => $event->id)); +?> +<?= $this->translate('Timestamp') ?>: <?= $event->created ?> + +<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?> +<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?> + +<?= $this->translate('Type') ?>: <?= $event->getType() ?> + +<?php foreach (array('host_name', 'program', 'message') as $col): ?> +<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?> + +<?php endforeach ?> + +<?php endforeach ?> diff --git a/application/views/scripts/events/details.phtml b/application/views/scripts/events/details.phtml new file mode 100644 index 0000000..ee9f3cd --- /dev/null +++ b/application/views/scripts/events/details.phtml @@ -0,0 +1,50 @@ +<?php +/** @var array $events */ + +if (! $this->compact): +?> +<div class="controls"> + <?= $this->tabs ?> +</div> +<?php endif ?> +<div class="content"> + <table class="common-table event-summary-table table-row-selectable" data-base-target="_next"> + <?php $autoClear = false; foreach($events as $eventData): + /** @var \Icinga\Module\Eventdb\Event $event */ + $event = $this->event($eventData); + $url = $this->url('eventdb/event', array('id' => $event->id)); + ?> + <tr href="<?= $url ?>"> + <td class="priority-col <?= $event->getPriority() ?> <?= $event->ack ? 'ack' : '' ?>"> + <div class="priority-label"><?= strtoupper($event->getPriority()) ?></div> + <div class="event-meta"><span class="timeago" title="<?= $event->created ?>"><?= $this->timeAgo(strtotime($event->created)) ?></span></div> + </td> + <td class="icon-col"> + <?= $this->icon($event->getTypeIcon(), $event->getType()) ?> + <?php if ($event->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?> + <?php if ($event->group_autoclear) { $autoClear = true; echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?> + </td> + <?= $this->column('host_name', $event) ?> + <?= $this->column('program', $event) ?> + <?= $this->column('message', $event) ?> + </tr> + <?php endforeach; ?> + </table> + + <?php + foreach ($extensionsHtml as $extensionHtml) { + echo $extensionHtml; + } + ?> + + <?php if (isset($commentForm)): ?> + <h3><?= $this->translate('Add comment / acknowledge') ?></h3> + <?php if ($autoClear): ?> + <div class="warning"> + <?= $this->translate('At least one event is set to auto-clear.') ?> + <?= $this->translate('Please only acknowledge manually, if you know what that means.') ?> + </div> + <?php endif; ?> + <div class="comment-form"><?= $commentForm ?></div> + <?php endif ?> +</div> diff --git a/application/views/scripts/events/index-plain.phtml b/application/views/scripts/events/index-plain.phtml new file mode 100644 index 0000000..55f4c2e --- /dev/null +++ b/application/views/scripts/events/index-plain.phtml @@ -0,0 +1,19 @@ +<?php /** @var \Icinga\Repository\RepositoryQuery $events */ +if (! $events->hasResult()): ?> +<?= $this->translate('No events recorded yet.') ?> +<?php else: +$displayColumns = array_merge(array('host_name', 'program', 'message', 'facility'), $additionalColumns); +foreach ($events as $eventData): /** @var \Icinga\Module\Eventdb\Event $event */ $event = $this->event($eventData); ?> +<?= $this->translate('Timestamp') ?>: <?= $event->created ?> + +<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?> +<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?> + +<?= $this->translate('Type') ?>: <?= $event->getType() ?> + +<?php foreach ($displayColumns as $col): ?> +<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?> + +<?php endforeach ?> + +<?php endforeach; endif; ?> diff --git a/application/views/scripts/events/index.phtml b/application/views/scripts/events/index.phtml new file mode 100644 index 0000000..f1e4da7 --- /dev/null +++ b/application/views/scripts/events/index.phtml @@ -0,0 +1,93 @@ +<?php if (! $this->compact): ?> +<div class="controls"> + <?= $this->tabs ?> + <?php + $helpMessage = $this->translate( + 'Press and hold the Ctrl key while clicking on rows to select multiple rows or press and hold the Shift key to' + . ' select a range of rows', + 'Multi-selection help' + ); + ?> + <div class="selection-info" title="<?= $this->escape($helpMessage) ?>"> + <?= sprintf( + /// TRANSLATORS: Please leave %s as it is because the selection counter is wrapped in a span tag for updating + /// the counter via JavaScript + $this->translate('%s row(s) selected', 'Multi-selection count'), + '<span class="selection-info-count">0</span>' + ) ?> + </div> + <?= $this->paginator ?> + <div class="sort-controls-container"> + <?= $this->limiter ?> + <?= $this->sortBox ?> + </div> + <?= $this->filterEditor ?> + <div class="quick-filter-controls"> + <?= $this->severityFilterForm ?> + <?= $this->ackFilterForm ?> + </div> +</div> +<?php endif ?> +<div class="content"> +<?php /** @var \Icinga\Repository\RepositoryQuery $events */if (! $events->hasResult()): ?> + <p><?= $this->translate('No events recorded yet.') ?></p> +</div> +<?php return; endif; $displayColumns = array_merge(array('host_name', 'program', 'message', 'facility'), $additionalColumns); ?> + <table class="common-table table-row-selectable multiselect table-responsive events-table" + data-base-target="_next" + data-icinga-multiselect-url="<?= $this->href('eventdb/events/details') ?>" + data-icinga-multiselect-controllers="<?= $this->href('eventdb/events') ?>" + data-icinga-multiselect-data="id"> + <?php if (! $this->compact): ?> + <thead> + <tr> + <th></th> + <th></th> + <?php foreach ($displayColumns as $displayColumn): ?> + <?= $this->columnHeader($displayColumn) ?> + <?php endforeach ?> + </tr> + </thead> + <?php endif; ?> + <tbody> + <?php + foreach ($events as $eventData): + /** @var \Icinga\Module\Eventdb\Event $event */ + $event = $this->event($eventData); + $created = $event->created; + $createdTs = strtotime($created); + $url = $this->url('eventdb/event', array('id' => $event->id)); + $classes = array('priority-col', $event->getPriority()); + if ($event->ack) { + $classes[] = 'ack'; + } + ?> + <tr href="<?= $url ?>"> + <td class="<?= implode(' ', $classes) ?>"> + <div class="priority-label"><?= strtoupper($event->getPriority()) ?></div> + <div class="event-meta"><a href="<?= $url ?>" class="timeago" title="<?= $created ?>"><?= $this->timeAgo($createdTs) ?></a></div> + </td> + <td class="icon-col"> + <?= $this->icon($event->getTypeIcon(), $event->getType()) ?> + <?php if ($event->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?> + <?php if ($event->group_autoclear) { echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?> + </td> + <?php foreach ($displayColumns as $displayColumn): ?> + <?= $this->column($displayColumn, $event) ?> + <?php endforeach ?> + </tr> + <?php endforeach ?> + </tbody> + </table> +<?php if ($this->compact && $events->hasMore()): ?> + <?= $this->qlink( + $this->translate('Show More'), + $this->url()->without(array('view', 'limit')), + null, + array( + 'data-base-target' => '_next', + 'class' => 'action-link' + ) + ) ?> +<?php endif; ?> +</div> diff --git a/application/views/scripts/format/text.phtml b/application/views/scripts/format/text.phtml new file mode 100644 index 0000000..647914b --- /dev/null +++ b/application/views/scripts/format/text.phtml @@ -0,0 +1,13 @@ +<?php +/** @var string $text */ +/** @var string|null $partial */ +if (! $this->compact): ?> +<div class="controls"><?= $this->tabs ?></div> +<?php endif ?> +<div class="content"> +<?php if ($partial !== null): ?> + <pre class="copyable"><?= $this->partial($partial . '.phtml', null, $this) ?></pre> +<?php else: ?> + <pre class="copyable"><?= htmlspecialchars($text) ?></pre> +<?php endif ?> +</div> diff --git a/configuration.php b/configuration.php new file mode 100644 index 0000000..0147b89 --- /dev/null +++ b/configuration.php @@ -0,0 +1,46 @@ +<?php +/** @var Icinga\Application\Modules\Module $this */ + +$config = $this->getConfig(); + +$url = 'eventdb/events'; +if (($default_filter = $config->get('global', 'default_filter')) !== null) { + $url .= '?' . $default_filter; +} + +$section = $this->menuSection('EventDB', array( + 'icon' => 'tasks', + 'priority' => 200, + 'url' => $url, +)); + +$this->provideConfigTab('config', array( + 'title' => $this->translate('Configure EventDB'), + 'label' => $this->translate('Config'), + 'url' => 'config' +)); +$this->provideConfigTab('monitoring', array( + 'title' => $this->translate('Configure integration into the monitoring module'), + 'label' => $this->translate('Monitoring'), + 'url' => 'config/monitoring' +)); + +$this->providePermission( + 'eventdb/events', + $this->translate('Allow to view events') +); + +$this->providePermission( + 'eventdb/comments', + $this->translate('Allow to view comments') +); + +$this->providePermission( + 'eventdb/interact', + $this->translate('Allow to acknowledge and comment events') +); + +$this->provideRestriction( + 'eventdb/events/filter', + $this->translate('Restrict views to the events that match the filter') +); diff --git a/doc/02-Configuration.md b/doc/02-Configuration.md new file mode 100644 index 0000000..0f6b04e --- /dev/null +++ b/doc/02-Configuration.md @@ -0,0 +1,53 @@ +Configuration +============= + +## Database Resource + +To let the module know where the events are stored you have to create an SQL +database resource, with the EventDB schema, and events collected by EventDB. + +Access to the EventDB for the module is handled with normal Icingaweb2 resources. + +Create the resource and go to the config area to select it: + + Configuration -> Modules -> Eventdb -> Backend + +![Configuration Backend](screenshots/configuration-backend.png) + +## Monitoring Integration + +The EventDB module integrates into Icinga Web 2's monitoring module by default, +offering action links in host and service detail views. + +![Configuration Monitoring](screenshots/configuration-monitoring.png) + +### Default actions + +By default, every host and services shows an action link to the event list, +filtered by host name. + +### Custom Variable + +You can configure a custom variable that enables the integration selectively. + +* `_edb` or `vars.edb` will enable the actions only on objects that have the custom var +* `_edb_filter` or `vars.edb_filter` allows you to pre-filter the linked events + +The name of the customvar (`edb`) needs to be configured in the config area of the module. + +Also see [Custom Variables](03-CustomVars.md) documentation. + +### Always show actions + +There are options to always show actions on host or service, even if the custom variable +is not set. + +### Detail view + +Icingaweb adds a new feature in 2.5.0 to allow extra content inside the detail views +of hosts and services. + +By default the view filters for not acknowledged events, and shows you the last, most +critical events first. One can jump to events list or a detail view immediately. + +Custom filters can be added in the config area, also the view can be disabled there.
\ No newline at end of file diff --git a/doc/03-CustomVars.md b/doc/03-CustomVars.md new file mode 100644 index 0000000..cfd4b74 --- /dev/null +++ b/doc/03-CustomVars.md @@ -0,0 +1,50 @@ +Custom Variables +================ + +The monitoring integration of this module can use custom variables from the +Icinga context to control display and filtering of the integration. + +Also see [Configuration](02-Configuration.md) on how to configure the features. + +Custom variables control: + +* If the EventDB integration and actions are shown for a host or service +* How the linked results should be filtered + +## Examples + +For Icinga 2: + +```icinga2 +object Host "test" { + import "generic-host" + + address = "127.0.0.1" + + vars.edb = "1" + vars.edb_filter = "priority!=7&priority!=5&priority!=6&ack=0" + // ... +} +``` + +For Icinga 1.x: + +```nagios +define host { + use generic-host + host_name test + + address 127.0.0.1 + + _edb 1 + _edb_filter priority!=7&priority!=5&priority!=6&ack=0 +} +``` + +**Note:** A filter by `host_name` will always be added, unless you have `host_name` as part of your filter. + +## Legacy filters + +The module also supports legacy JSON filters from the icinga-web 1.x EventDB module. + +Please see the `examples` directory of this module for some supported filters. diff --git a/doc/09-Security.md b/doc/09-Security.md new file mode 100644 index 0000000..9aab433 --- /dev/null +++ b/doc/09-Security.md @@ -0,0 +1,26 @@ +Security +============================ + +The EventDB Module provides permissions and restrictions as described below. + +## Permissions + +| Name | Description | +| ---------------- | ----------- | +| eventdb/events | Allow to view events | +| eventdb/comments | Allow to view comments | +| eventdb/interact | Allow to acknowledge and comment events | + +## Restrictions + +| Name | Description | +| ----------------------- | ----------- | +| eventdb/events/filter | Restrict views to the events that match the filter | +| eventdb/comments/filter | Restrict views to the comments that match the filter | + +## Examples + +| eventdb/events/filter | Description | +| -------------------------- | ----------- | +| type!=syslog | Hide the Syslog events from a role | +| type=syslog&program=icinga | Show only Icinga-related Syslog events to a role | diff --git a/doc/10-Screenshots.md b/doc/10-Screenshots.md new file mode 100644 index 0000000..22905d3 --- /dev/null +++ b/doc/10-Screenshots.md @@ -0,0 +1,24 @@ +Screenshots +=========== + +**Overview of Events** + +![Screenshot](screenshots/overview.png) + +**Filtered overview** + +![Screenshot](screenshots/overview-filtered.png) + +**Detail area** + +![Screenshot](screenshots/overview-with-details.png) + +**Monitoring actions** + +![Screenshot](screenshots/monitoring-actions.png) + +**Monitoring detail view** + +For Icinga Web 2 >= 2.5.0 + +![Screenshot](screenshots/monitoring-detailview.png)
\ No newline at end of file diff --git a/doc/screenshots/configuration-backend.png b/doc/screenshots/configuration-backend.png Binary files differnew file mode 100644 index 0000000..6de9fba --- /dev/null +++ b/doc/screenshots/configuration-backend.png diff --git a/doc/screenshots/configuration-monitoring.png b/doc/screenshots/configuration-monitoring.png Binary files differnew file mode 100644 index 0000000..d6c9e8d --- /dev/null +++ b/doc/screenshots/configuration-monitoring.png diff --git a/doc/screenshots/monitoring-actions.png b/doc/screenshots/monitoring-actions.png Binary files differnew file mode 100644 index 0000000..724e0a0 --- /dev/null +++ b/doc/screenshots/monitoring-actions.png diff --git a/doc/screenshots/monitoring-detailview.png b/doc/screenshots/monitoring-detailview.png Binary files differnew file mode 100644 index 0000000..e7ceb83 --- /dev/null +++ b/doc/screenshots/monitoring-detailview.png diff --git a/doc/screenshots/overview-filtered.png b/doc/screenshots/overview-filtered.png Binary files differnew file mode 100644 index 0000000..d183cd8 --- /dev/null +++ b/doc/screenshots/overview-filtered.png diff --git a/doc/screenshots/overview-with-details.png b/doc/screenshots/overview-with-details.png Binary files differnew file mode 100644 index 0000000..53a6008 --- /dev/null +++ b/doc/screenshots/overview-with-details.png diff --git a/doc/screenshots/overview.png b/doc/screenshots/overview.png Binary files differnew file mode 100644 index 0000000..99cc82d --- /dev/null +++ b/doc/screenshots/overview.png diff --git a/examples/legacy-filter/column-integration.json b/examples/legacy-filter/column-integration.json new file mode 100644 index 0000000..474031f --- /dev/null +++ b/examples/legacy-filter/column-integration.json @@ -0,0 +1,50 @@ +/* + NOTE: this is a JSON file with comments. You might not be able to parse it, depending on the implementation! + + These are the filter features mapped by the edbColumn.js integration. + + See https://git.netways.org/eventdb/eventdb/blob/master/icinga-cronk/EventDB/lib/js/edbColumn.js +*/ +{ + "hostFilter": { + // Note: `data.host` is by default the hostname, unless set in edb_filter + "include_pattern": "{{mapped from data.host}}", + "include_pattern_type": "regexp", + "exclude_pattern_type": "disabled", + "exclude_pattern": false, + "include_set": [], + "exclude_set": [] + }, + "programFilter": { + "include_pattern": false, + "include_pattern_type": "disabled", + "exclude_pattern": false, + "exclude_pattern_type": "disabled", + "include_set": [ /* mapped from data.programInclusion */ ], + "exclude_set": [ /* mapped from data.programExclusion */ ] + }, + "messageFilter": { + "items": [ /* mapped from data.msg */ ] + }, + "misc": { + "hideAck": false + }, + "sourceExclusion": [ /* mapped from data.sourceExclusion */ ], + "priorityExclusion": [ /* mapped from data.priorityExclusion */ ], + "facilityExclusion": [ /* mapped from data.facilityExclusion */ ], + "timespan": { + "from": "{{ mapped from data.startTime }}", + "to": -1 + }, + "display": { + "order": { + "field": "created", + "dir": "desc" + }, + "group": { + "field": null + }, + "count": "id", + "limit": 50 + } +} diff --git a/examples/legacy-filter/examples.md b/examples/legacy-filter/examples.md new file mode 100644 index 0000000..fdcba03 --- /dev/null +++ b/examples/legacy-filter/examples.md @@ -0,0 +1,9 @@ +Legacy Filter JSON examples +=========================== + +Here are some JSON examples, each line is a single filter used in `edb_filter` custom variable. + + { host: 'otherhostname' } + { host: 'specialhostname', priorityExclusion: [] } + { "host": ".*", "programInclusion": ["cloud-monitoring"] } + { programInclusion: ['test-program'] } diff --git a/examples/legacy-filter/full-filter.json b/examples/legacy-filter/full-filter.json new file mode 100644 index 0000000..5d89277 --- /dev/null +++ b/examples/legacy-filter/full-filter.json @@ -0,0 +1,77 @@ +/* + NOTE: this is a JSON file with comments. You might not be able to parse it, depending on the implementation! + + This is the full filter style, that the old EventDB Cronk supported. + + The old monitoring integration *did not* support the exact filter. +*/ +{ + "hostFilter": { + "include_pattern": "test", + "include_pattern_type": "exact", + "exclude_pattern_type": "regexp", + "exclude_pattern": "aa.*b", + "include_set": [ + "test" + ], + "exclude_set": [] + }, + "programFilter": { + "include_pattern": false, + "include_pattern_type": "disabled", + "exclude_pattern": "test", + "exclude_pattern_type": "contains", + "include_set": [ + "" + ], + "exclude_set": [] + }, + "messageFilter": { + "items": [ + { + "type": "exc", + "message": "onlydebug", + "isRegexp": false + }, + { + "type": "inc", + "message": "needthis.*", + "isRegexp": true + } + ] + }, + "misc": { + "hideAck": true + }, + "sourceExclusion": [ + "2", + "3", + "4" + ], + "priorityExclusion": [ + "5", + "6", + "7" + ], + "facilityExclusion": [ + "8", + "9", + "10", + "11" + ], + "timespan": { + "from": 1502362200, + "to": 1502362200 + }, + "display": { + "order": { + "field": "created", + "dir": "desc" + }, + "group": { + "field": null + }, + "count": "id", + "limit": 200 + } +}
\ No newline at end of file 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); + } + } +} diff --git a/module.info b/module.info new file mode 100644 index 0000000..4461db0 --- /dev/null +++ b/module.info @@ -0,0 +1,8 @@ +Module: eventdb +Version: 1.3.0 +Description: EventDB module +Depends: monitoring>=2.5.0 +Description: Integration to browse and acknowledge event in the EventDB + EventDB is a event storage and search addon to Icinga. + + This module provides a simple access and integration with monitoring. diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..f9b092b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="test/bootstrap.php" + > + <testsuites> + <testsuite name="EventDB PHP Unit tests"> + <directory suffix=".php">test/php</directory> + </testsuite> + </testsuites> +</phpunit> diff --git a/public/css/module.less b/public/css/module.less new file mode 100644 index 0000000..9c86a8f --- /dev/null +++ b/public/css/module.less @@ -0,0 +1,285 @@ +@color-priority-emergency: #ff2200; +@color-priority-emergency-fg: white; +@color-priority-alert: #ff4400; +@color-priority-alert-fg: white; +@color-priority-critical: #ff6600; +@color-priority-critical-fg: white; +@color-priority-error: #ff8800; +@color-priority-error-fg: white; +@color-priority-warning: #ffaa44; // same as @color-warning +@color-priority-warning-fg: #6d572b; +@color-priority-notice: #44bb77; // same as @color-ok; +@color-priority-notice-fg: white; +@color-priority-information: #aaaaff; +@color-priority-information-fg: #32486d; +@color-priority-debug: #ccc; +@color-priority-debug-fg: #333; + +h2 { + margin-top: 1em; +} + +.events-table, .event-summary-table { + border-collapse: separate; + border-spacing: 0 1px; + padding: 0; +} + +.ack-col { + color: @icinga-blue; +} + +.priority-col { + width: 8em; + border-left: 5px solid; + white-space: nowrap; + text-align: center; + margin-right: 1em; + padding: 0.33em 1em; + + &.emergency { + border-left-color: @color-priority-emergency; + background-color: @color-priority-emergency; + color: @color-priority-emergency-fg; + + &.ack { + color: inherit; + background-color: inherit; + } + } + + &.critical { + border-left-color: @color-priority-critical; + background-color: @color-priority-critical; + color: @color-priority-critical-fg; + + &.ack { + color: inherit; + background-color: inherit; + } + } + + &.alert { + border-left-color: @color-priority-alert; + background-color: @color-priority-alert; + color: @color-priority-alert-fg; + + &.ack { + color: inherit; + background-color: inherit; + } + } + + &.error { + border-left-color: @color-priority-error; + background-color: @color-priority-error; + color: @color-priority-error-fg; + + &.ack { + color: inherit; + background-color: inherit; + } + } + + &.warning { + border-left-color: @color-priority-warning; + background-color: @color-priority-warning; + color: @color-priority-warning-fg; + + &.ack { + color: inherit; + background-color: inherit; + } + } + + &.notice { + border-left-color: @color-priority-notice; + } + + &.information, &.info { + border-left-color: @color-priority-information; + } + + &.debug { + border-left-color: @color-priority-debug; + } +} + +.comment-link { + .button-link(); + + margin-top: 0.5em; +} + +.quick-filter-controls { + margin-top: 0.5em; + text-align: center; +} + +.ack-filter-form { + margin-left: 0.5em; +} + +.severity-filter-form { + .control-label-group { + display: none; + } + + .control-group { + display: inline-block; + padding: 0; + } + + .active { + font-weight: bold; + } + + .emergency.active { + background-color: @color-priority-emergency; + border-color: @color-priority-emergency; + color: @color-priority-emergency-fg; + } + + .alert.active { + background-color: @color-priority-alert; + border-color: @color-priority-alert; + color: @color-priority-alert-fg; + } + + .critical.active { + background-color: @color-priority-critical; + border-color: @color-priority-critical; + color: @color-priority-critical-fg; + } + + .error.active { + background-color: @color-priority-error; + border-color: @color-priority-error; + color: @color-priority-error-fg; + } + + .warning.active { + background-color: @color-priority-warning; + border-color: @color-priority-warning; + color: @color-priority-warning-fg; + } + + .notice.active { + background-color: @color-priority-notice; + border-color: @color-priority-notice; + color: @color-priority-notice-fg; + } + + .information.active, .info.active { + background-color: @color-priority-information; + border-color: @color-priority-information; + color: @color-priority-information-fg; + } + + .debug.active { + background-color: @color-priority-debug; + border-color: @color-priority-debug; + color: @color-priority-debug-fg; + } +} + +.events-table, .event-summary-table { + .event-message { + max-width: 60em; + font-family: @font-family-fixed; + } +} + +.event-message { + &.detail { + font-size: 1.2em; + border-left: 5px solid #eee; + padding: 0.66em 0.33em; + } + + a { + font-weight: bold; + } +} + +.event-summary-table { + .event-host_address { + font-family: @font-family-fixed; + font-size: 0.9em; + color: @gray; + } +} + +a.action-link { + color: @icinga-blue !important; + margin-right: 1em; +} + +.name-value-table { + th { + width: 8em; + } +} + +.comments-table { + width: auto; + + tr { + border-bottom: none; + } + + td { + padding: 0.5em 0.5em; + } + + .comment-created { + white-space: nowrap; + } + .comment-type { + max-width: 2em; + } + .comment-message { + min-width: 30em; + font-family: @font-family-fixed; + } +} + +.comment-form { + select { + width: auto; + } + + input[name=comment] { + width: 25em; + } + + input[type=submit] { + padding: 0.2em 0.5em; + } +} + +div.warning { + display: inline-block; + font-size: 1.2em; + font-weight: bold; + border: 1px solid @color-priority-warning; + border-left: 5px solid @color-priority-warning; + margin: 0.5em 0; + padding: 0.3em 0.5em; +} + +.copyable-actions { + margin-bottom: 0.5em; +} + +input[type=submit].disabled, +button.disabled { + border-color: @gray-light !important; + background-color: white !important; + color: @gray-light !important; + + &:hover { + background: @gray-light !important; + color: white; + } +} diff --git a/public/js/module.js b/public/js/module.js new file mode 100644 index 0000000..0678421 --- /dev/null +++ b/public/js/module.js @@ -0,0 +1,97 @@ +/*! Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +;(function(Icinga) { + + var EventDB = function(module) { + this.module = module; + this.initialize(); + }; + + EventDB.prototype = { + initialize: function() { + this.module.on('rendered', this.enableCopyable); + this.module.on('submit', 'form.severity-filter-form', this.severitySubmit); + + var addCSSRule = function(sheet, selector, rules, index) { + if('insertRule' in sheet) { + sheet.insertRule(selector + '{' + rules + '}', index); + } else if('addRule' in sheet) { + sheet.addRule(selector, rules, index); + } else { + this.module.icinga.logger.debug('Can\'t insert CSS rule'); + } + }; + + var sheet = (function() { + var style = document.createElement('style'); + // WebKit hack + style.appendChild(document.createTextNode('')); + document.head.appendChild(style); + return style.sheet; + })(); + + addCSSRule( + sheet, + '#layout.twocols.wide-layout #col1.module-eventdb, #layout.twocols.wide-layout #col1.module-eventdb ~ #col2', + 'width: 50%', + 0 + ); + }, + enableCopyable: function() { + var e = this; + $('.copyable').each(function() { + var $button = $('<a>') + .attr('href', '#') + .addClass('action-link icon icon-globe copyable-button') + .text('Copy text'); + + $button.on('click', function() { + var $el = $(this).parent().siblings('.copyable'); + if ($el) { + e.selectText($el[0]); + document.execCommand('copy'); + setTimeout(function () { + e.clearSelection() + }, 500); + if (icinga) { + icinga.loader.createNotice('info', 'Text copied to clipboard') + } + } + }); + + var $div = $('<div>').addClass('copyable-actions').append($button); + + $(this).before($div); + }); + }, + selectText: function (text) { + var doc = document, range, selection; + if (doc.body.createTextRange) { + range = document.body.createTextRange(); + range.moveToElementText(text); + range.select(); + } else if (window.getSelection) { + selection = window.getSelection(); + range = document.createRange(); + range.selectNodeContents(text); + selection.removeAllRanges(); + selection.addRange(range); + } + }, + clearSelection: function() { + if (document.selection) { + document.selection.empty(); + } else if (window.getSelection) { + window.getSelection().removeAllRanges(); + } + }, + severitySubmit: function(ev) { + $(ev.currentTarget) + .find('input[type=submit]') + .prop('disabled', true) + .addClass('disabled'); + } + }; + + Icinga.availableModules.eventdb = EventDB; +}(Icinga)); @@ -0,0 +1,9 @@ +<?php + +use Icinga\Application\Modules\Module; + +/** @var Module $this */ +$this->provideHook('monitoring/HostActions'); +$this->provideHook('monitoring/ServiceActions'); + +$this->provideHook('monitoring/DetailviewExtension'); diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..0253904 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,16 @@ +<?php + +use Icinga\Module\Eventdb\Test\Bootstrap; + +call_user_func(function () { + $basedir = dirname(__DIR__); + if (! class_exists('PHPUnit_Framework_TestCase')) { + require_once __DIR__ . '/phpunit-compat.php'; + } + + $include_path = $basedir . '/vendor' . PATH_SEPARATOR . ini_get('include_path'); + ini_set('include_path', $include_path); + + require_once $basedir . '/library/Eventdb/Test/Bootstrap.php'; + Bootstrap::web($basedir); +}); diff --git a/test/config/authentication.ini b/test/config/authentication.ini new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/config/authentication.ini diff --git a/test/config/config.ini b/test/config/config.ini new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/config/config.ini diff --git a/test/config/resources.ini b/test/config/resources.ini new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/config/resources.ini diff --git a/test/php/library/Eventdb/Data/LegacyFilterParserTest.php b/test/php/library/Eventdb/Data/LegacyFilterParserTest.php new file mode 100644 index 0000000..51b34ab --- /dev/null +++ b/test/php/library/Eventdb/Data/LegacyFilterParserTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Tests\Icinga\Module\Eventdb\CustomVariable; + +use Icinga\Module\Eventdb\Data\LegacyFilterParser; +use Icinga\Module\Eventdb\Test\BaseTestCase; + +class LegacyFilterParserTest extends BaseTestCase +{ + /** + * Some filter examples that should work and match the result + * + * Note: This is not always clean JSON, but it should work! + * + * @var array + */ + protected $validFilters = array( + '{}' => 'host_name=testhost', // default filter + '{ host: "test" }' => 'host_name=test', + "{ host: 'test' }" => 'host_name=test', + "{ host: 'otherhostname' }" => 'host_name=otherhostname', + "{ host: 'specialhostname', priorityExclusion: [] }" => 'host_name=specialhostname', + "{ host: 'specialhostname', priorityExclusion: [6,7,8] }" => 'host_name=specialhostname&priority!=6&priority!=7&priority!=8', + '{ "host": "*" }' => '', // doesn't make much sense, but well... + '{ "host": "*", "programInclusion": ["cloud-monitoring"] }' => 'program=cloud-monitoring', + '{ "host": ".*", "programInclusion": ["cloud-monitoring"] }' => 'program=cloud-monitoring', + '{ "host": "myhost.*.example.com" }' => 'host_name=myhost%2A.example.com', + "{ programInclusion: ['test1', 'test2'] }" => 'host_name=testhost&(program=test1|program=test2)', + "{ programInclusion: ['test'] }" => 'host_name=testhost&program=test', + "{ programExclusion: ['test'] }" => 'host_name=testhost&program!=test', + "{ programExclusion: ['test1', 'test2'] }" => 'host_name=testhost&program!=test1&program!=test2', + ); + + public function testFiltersThatContainSomeJson() + { + $filters = array( + ' { host: "test" } ', + ' {} ', + '{}', + "{\n\"multiline\": 1\n}", + ); + foreach ($filters as $filter) { + $this->assertTrue(LegacyFilterParser::isJsonFilter($filter)); + } + } + + public function testFiltersThatDoNotContainJson() + { + $filters = array( + ' {xxxx ', + 1337, + 'sometext', + "{\nbrokenjson\n", + ); + foreach ($filters as $filter) { + $this->assertFalse(LegacyFilterParser::isJsonFilter($filter), 'Filter: ' . $filter); + } + + } + + public function testParsingFilters() + { + foreach ($this->validFilters as $json => $result) { + $this->assertTrue( + LegacyFilterParser::isJsonFilter($json), + 'Should be recognized as JSON filter by isJsonFilter' + ); + + $filter = LegacyFilterParser::parse($json, 'testhost'); + $this->assertEquals( + $result, + $filter->toQueryString(), + 'Resulting URL filter should match for json: ' . $json + ); + } + } +} diff --git a/test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php b/test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php new file mode 100644 index 0000000..d833d73 --- /dev/null +++ b/test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php @@ -0,0 +1,186 @@ +<?php + +namespace Tests\Icinga\Module\Eventdb\ProvidedHook\Monitoring; + +use Icinga\Application\Config; +use Icinga\Module\Eventdb\ProvidedHook\Monitoring\EventdbActionHook; +use Icinga\Module\Eventdb\Test\BaseTestCase; +use Icinga\Module\Eventdb\Test\PseudoHost; +use Icinga\Module\Eventdb\Test\PseudoMonitoringBackend; +use Icinga\Module\Eventdb\Test\PseudoService; +use Icinga\Web\Navigation\NavigationItem; + +class EventdbActionHookTest extends BaseTestCase +{ + public function setUp() + { + parent::setUp(); + EventdbActionHook::wantCache(false); + } + + public function testHostWithoutVarsAndNoConfig() + { + $this->setupConfiguration(null, null, null); + + $nav = EventdbActionHook::getActions($this->buildHost(null, null)); + + $items = $nav->getItems(); + $this->assertCount(1, $items); + + /** @var NavigationItem $navObj */ + $navObj = current($items); + + $this->assertEquals('host_name=testhost', $navObj->getUrl()->getQueryString()); + } + + public function testHostWithoutVarsAndNormalConfig() + { + $this->setupConfiguration(); + + $nav = EventdbActionHook::getActions($this->buildHost(null, null)); + + $this->assertCount(0, $nav->getItems()); + } + + public function testHostWithVars() + { + $this->setupConfiguration(); + + $nav = EventdbActionHook::getActions($this->buildHost()); + + $items = $nav->getItems(); + $this->assertCount(1, $items); + + /** @var NavigationItem $navObj */ + $navObj = current($items); + + $this->assertEquals('host_name=testhost', $navObj->getUrl()->getQueryString()); + } + + public function testHostWithVarsAlwaysOn() + { + $this->setupConfiguration('edb', '1'); + + $nav = EventdbActionHook::getActions($this->buildHost(null, 'othervar')); + + $this->assertCount(1, $nav->getItems()); + } + + public function testServiceWithVarsAlwaysOn() + { + $this->setupConfiguration('edb', null, '1'); + + $nav = EventdbActionHook::getActions($this->buildService(null, 'othervar')); + + $this->assertCount(1, $nav->getItems()); + } + + public function testHostWithLegacyFilter() + { + $this->setupConfiguration(); + + $nav = EventdbActionHook::getActions($this->buildHost("{ host: 'test2', programInclusion: 'test2' }")); + + $items = $nav->getItems(); + $this->assertCount(2, $items); + + /** @var NavigationItem $navObj */ + $navObj = current($items); + + $this->assertEquals('host_name=test2&program=test2', $navObj->getUrl()->getQueryString()); + } + + public function testHostWithFilter() + { + $this->setupConfiguration(); + + $nav = EventdbActionHook::getActions($this->buildHost("program=test3")); + + $items = $nav->getItems(); + $this->assertCount(2, $items); + + /** @var NavigationItem $navObj */ + $navObj = current($items); + + $this->assertEquals("program=test3&host_name=testhost", $navObj->getUrl()->getQueryString()); + } + + public function testHostWithFilterThatFiltersHost() + { + $this->setupConfiguration(); + + $nav = EventdbActionHook::getActions($this->buildHost("host_name=test3&program=test3")); + + $items = $nav->getItems(); + $this->assertCount(2, $items); + + /** @var NavigationItem $navObj */ + $navObj = current($items); + + $this->assertEquals("host_name=test3&program=test3", $navObj->getUrl()->getQueryString()); + } + + protected function configure($settings = array()) + { + $config = Config::module('eventdb'); + $section = $config->getSection('monitoring'); + foreach ($settings as $key => $val) { + $section->$key = $val; + } + $config->setSection('monitoring', $section); + + // NOTE: we need to save here, because Config::module always load config from disk + $config->saveIni(); + + return $this; + } + + protected function setupConfiguration($custom_var = 'edb', $always_host = null, $always_service = null) + { + $this->configure(array( + 'custom_var' => $custom_var, + 'always_on_host' => $always_host, + 'always_on_service' => $always_service, + )); + } + + protected function monitoringBackend() + { + return PseudoMonitoringBackend::dummy(); + } + + protected function buildHost($plainFilter = null, $custom_var = 'edb') + { + $host = new PseudoHost($this->monitoringBackend(), 'testhost'); + $host->host_name = 'testhost'; + + $vars = array(); + if ($custom_var !== null) { + $vars[$custom_var] = '1'; + } + if ($plainFilter !== null) { + $vars[$custom_var . '_filter'] = $plainFilter; + } + $host->provideCustomVars($vars); + + return $host; + } + + protected function buildService($plainFilter = null, $custom_var = 'edb') + { + $service = new PseudoService($this->monitoringBackend(), 'testhost', 'test'); + $service->host_name = 'testhost'; + $service->service_description = 'testhost'; + + $vars = array(); + if ($custom_var !== null) { + $vars[$custom_var] = '1'; + } + if ($plainFilter !== null) { + $vars[$custom_var . '_filter'] = $plainFilter; + } + $service->provideCustomVars($vars); + + return $service; + } +} diff --git a/test/phpunit-compat.php b/test/phpunit-compat.php new file mode 100644 index 0000000..2b1be3a --- /dev/null +++ b/test/phpunit-compat.php @@ -0,0 +1,10 @@ +<?php + +use PHPUnit\Framework\TestCase; + +/** + * @codingStandardsIgnoreStart + */ +class PHPUnit_Framework_TestCase extends TestCase +{ +} diff --git a/test/setup_vendor.sh b/test/setup_vendor.sh new file mode 100755 index 0000000..742dfcd --- /dev/null +++ b/test/setup_vendor.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -ex + +MODULE_HOME=${MODULE_HOME:="$(dirname "$(readlink -f $(dirname "$0"))")"} +PHP_VERSION="$(php -r 'echo phpversion();')" + +ICINGAWEB_VERSION=${ICINGAWEB_VERSION:=2.4.1} +# TODO: Remove for 2.5.0 +if [[ "$ICINGAWEB_VERSION" =~ 2.4.* ]]; then + ICINGAWEB_GITREF=${ICINGAWEB_GITREF:="origin/master"} +fi + +PHPCS_VERSION=${PHPCS_VERSION:=2.9.1} + +if [ "$PHP_VERSION" '<' 5.6.0 ]; then + PHPUNIT_VERSION=${PHPUNIT_VERSION:=4.8} +else + PHPUNIT_VERSION=${PHPUNIT_VERSION:=5.7} +fi + +cd ${MODULE_HOME} + +test -d vendor || mkdir vendor +cd vendor/ + +# icingaweb2 +if [ -n "$ICINGAWEB_GITREF" ]; then + icingaweb_path="icingaweb2" + test ! -L "$icingaweb_path" || rm "$icingaweb_path" + + if [ ! -d "$icingaweb_path" ]; then + git clone https://github.com/Icinga/icingaweb2.git "$icingaweb_path" + fi + + ( + set -e + cd "$icingaweb_path" + git fetch -p + git checkout -f "$ICINGAWEB_GITREF" + ) +else + icingaweb_path="icingaweb2-${ICINGAWEB_VERSION}" + if [ ! -e "${icingaweb_path}".tar.gz ]; then + wget -O "${icingaweb_path}".tar.gz https://github.com/Icinga/icingaweb2/archive/v"${ICINGAWEB_VERSION}".tar.gz + fi + if [ ! -d "${icingaweb_path}" ]; then + tar xf "${icingaweb_path}".tar.gz + fi + + ln -svf "${icingaweb_path}" icingaweb2 +fi +ln -svf "${icingaweb_path}"/library/Icinga +ln -svf "${icingaweb_path}"/library/vendor/Zend + +# phpunit +phpunit_path="phpunit-${PHPUNIT_VERSION}" +if [ ! -e "${phpunit_path}".phar ]; then + wget -O "${phpunit_path}".phar https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar +fi +ln -svf "${phpunit_path}".phar phpunit.phar + +# phpcs +phpcs_path="phpcs-${PHPCS_VERSION}" +if [ ! -e "${phpcs_path}".phar ]; then + wget -O "${phpcs_path}".phar \ + https://github.com/squizlabs/PHP_CodeSniffer/releases/download/${PHPCS_VERSION}/phpcs.phar +fi +ln -svf "${phpcs_path}".phar phpcs.phar |