diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:42:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:42:52 +0000 |
commit | da0f8427204ad57aad08059906df0ea10a7ccf31 (patch) | |
tree | ccda92692e296a50dac104945f62b4402d5e4447 | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-cube-upstream.tar.xz icingaweb2-module-cube-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
53 files changed, 4723 insertions, 0 deletions
@@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..30110ac --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Icinga Cube + +[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/) +![Build Status](https://github.com/icinga/icingaweb2-module-cube/workflows/PHP%20Tests/badge.svg?branch=master) +[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-cube.svg)](https://github.com/Icinga/icingaweb2-module-cube) + +![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png) + +The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icingaweb2) +module. It currently shows host and service statistics (total count, health) grouped by +various custom variables in multiple dimensions. + +![Cube - Overview](doc/img/cube_simple.png) + +It will be your new best friend in case you are running a large environment and +want to get a quick answers to questions like: + +* Which project uses how many servers per environment at which location/site? + * Who occupies most servers? + * How many of those are used in production? + * Which project has only development and test boxes? +* Which operating system is used for which project and in which environment? + * Do we still have Debian Lenny? + * Which projects are to blame for this? + * Do we have applications where the operating systems used differ in staging + and production? +* Which project uses which operating system version for which application? + * Which projects have homogeneous environments? + * Which projects are at a consistent patch level? + * How many RHEL 6 variants (6.1, 6.2, 6.3...) do we use? + * Who is running the oldest ones? In production? +* Which projects are still using physical servers in which environment? + +For Businessmen - Drill and Slice +--------------------------------- + +Get answers to your questions. Quick and fully autonomous, using the cube +requires no technical skills. Choose amongst all available dimensions and rotate +the Cube to fit your needs. + +![Cube - Configure Dimensions](doc/img/cube_move-up.png) + +Want to drill down? Choose a slice and get your answers: + +![Cube - Configure Dimensions](doc/img/cube_slice.png) + +All facts configured for systems monitored by [Icinga](https://www.icinga.com/) +can be used for your research. + +For Icinga Director users +------------------------- + +![Cube - Action Links](doc/img/cube_action-links.png) + +In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director), +in addition to the multi-selection/edit feature the cube provides a nice way to +modify multiple hosts at once. + +![Cube - Director multi-edit](doc/img/cube_director.png) + +Installation +------------ + +To install Icinga Cube see [Installation](https://icinga.com/docs/icinga-cube/latest/doc/02-Installation/). diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php new file mode 100644 index 0000000..c3e5374 --- /dev/null +++ b/application/controllers/HostsController.php @@ -0,0 +1,30 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Cube\IcingaDb\IcingaDbHostStatusCube; +use Icinga\Module\Cube\Ido\IdoHostStatusCube; +use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Cube\Web\IdoController; + +class HostsController extends IdoController +{ + public function indexAction() + { + $this->createTabs()->activate('cube/hosts'); + + $this->renderCube(); + } + + protected function getCube() + { + if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) { + return new IcingaDbHostStatusCube(); + } + + return new IdoHostStatusCube(); + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..082fda3 --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,15 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Web\Controller; + +class IndexController extends Controller +{ + public function indexAction() + { + $this->redirectNow('cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString())); + } +} diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php new file mode 100644 index 0000000..ec08bbb --- /dev/null +++ b/application/controllers/ServicesController.php @@ -0,0 +1,30 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Cube\IcingaDb\IcingaDbServiceStatusCube; +use Icinga\Module\Cube\Ido\IdoServiceStatusCube; +use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Cube\Web\IdoController; + +class ServicesController extends IdoController +{ + public function indexAction() + { + $this->createTabs()->activate('cube/services'); + + $this->renderCube(); + } + + protected function getCube() + { + if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) { + return new IcingaDbServiceStatusCube(); + } + + return new IdoServiceStatusCube(); + } +} diff --git a/application/forms/DimensionsForm.php b/application/forms/DimensionsForm.php new file mode 100644 index 0000000..c4fcf73 --- /dev/null +++ b/application/forms/DimensionsForm.php @@ -0,0 +1,206 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Forms; + +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Dimension; +use Icinga\Module\Cube\DimensionParams; +use Icinga\Web\Form; +use Icinga\Web\Notification; + +class DimensionsForm extends Form +{ + /** + * @var Cube + */ + private $cube; + + public function setCube(Cube $cube) + { + $this->cube = $cube; + return $this; + } + + public function createElements(array $formData) + { + $cube = $this->cube; + $dimensions = $cube->listDimensions(); + $cnt = count($dimensions); + + if ($cnt < 3) { + $allDimensions = $cube->listAdditionalDimensions(); + + $this->addElement('select', 'addDimension', [ + 'multiOptions' => [null => $this->translate('+ Add a dimension')] + $allDimensions, + 'decorators' => ['ViewHelper'], + 'class' => 'autosubmit' + ]); + } + + $pos = 0; + foreach ($dimensions as $dimension) { + $this->addDimensionButtons($dimension, $pos++, $cnt); + } + + foreach ($cube->getSlices() as $key => $value) { + $this->addSlice($this->cube->getDimension($key), $value); + } + + $this->addAttribs(['class' => 'icinga-controls']); + } + + protected function addSlice(Dimension $dimension, $value) + { + $view = $this->getView(); + + $sliceId = sha1($dimension->getName()); + $this->addElement('button', 'removeSlice_' . $sliceId, [ + 'label' => $view->icon('cancel'), + 'decorators' => ['ViewHelper'], + 'value' => $dimension->getName(), + 'type' => 'submit', + 'escape' => false, + 'class' => 'dimension-control' + ]); + + $label = $view->escape( + sprintf( + '%s: %s = %s', + $view->translate('Slice/Filter'), + $dimension->getLabel(), + $value + ) + ); + + $this->addElement('note', 'slice_' . $sliceId, [ + 'class' => 'dimension-name', + 'value' => '<span class="dimension-name">' . $label . '</span>', + 'decorators' => ['ViewHelper'] + ]); + + $this->addDisplayGroup( + [ + 'removeSlice_' . $sliceId, + 'slice_' . $sliceId, + ], + $dimension->getName(), + [ + 'class' => 'dimensions', + 'decorators' => [ + 'FormElements', + 'Fieldset' + ] + ] + ); + } + + protected function addDimensionButtons(Dimension $dimension, $pos, $total) + { + $view = $this->getView(); + $dimensionId = sha1($dimension->getName()); + + $this->addElement('note', 'dimension_' . $dimensionId, [ + 'class' => 'dimension-name', + 'value' => '<span class="dimension-name">' . $view->escape($dimension->getLabel()) . '</span>', + 'decorators' => ['ViewHelper'] + ]); + + $this->addElement('button', 'removeDimension_' . $dimensionId, [ + 'label' => $view->icon('cancel'), + 'decorators' => ['ViewHelper'], + 'title' => sprintf($this->translate('Remove dimension "%s"'), $dimension->getLabel()), + 'value' => $dimension->getName(), + 'type' => 'submit', + 'escape' => false, + 'class' => 'dimension-control' + ]); + + if ($pos > 0) { + $this->addElement('button', 'moveDimensionUp_' . $dimensionId, [ + 'label' => $view->icon('angle-double-up'), + 'decorators' => ['ViewHelper'], + 'title' => sprintf($this->translate('Move dimension "%s" up'), $dimension->getLabel()), + 'value' => $dimension->getName(), + 'type' => 'submit', + 'escape' => false, + 'class' => 'dimension-control' + ]); + } + + if ($pos + 1 !== $total) { + $this->addElement('button', 'moveDimensionDown_' . $dimensionId, [ + 'label' => $view->icon('angle-double-down'), + 'decorators' => ['ViewHelper'], + 'title' => sprintf($this->translate('Move dimension "%s" down'), $dimension->getLabel()), + 'value' => $dimension->getName(), + 'type' => 'submit', + 'escape' => false, + 'class' => 'dimension-control' + ]); + } + + $this->addDisplayGroup( + [ + 'removeDimension_' . $dimensionId, + 'moveDimensionUp_' . $dimensionId, + 'moveDimensionDown_' . $dimensionId, + 'dimension_' . $dimensionId, + ], + $dimensionId, + [ + 'class' => 'dimensions', + 'decorators' => [ + 'FormElements', + 'Fieldset' + ] + ] + ); + } + + public function onSuccess() + { + $url = $this->getRequest()->getUrl(); + + if ($dimension = $this->getValue('addDimension')) { + $url->setParam('dimensions', DimensionParams::fromUrl($url)->add($dimension)->getParams()); + Notification::success($this->translate('New dimension has been added')); + } else { + $updateDimensions = false; + foreach ($this->cube->listDimensions() as $name => $_) { + $dimensionId = sha1($name); + + switch (true) { + case ($el = $this->getElement('removeDimension_' . $dimensionId)) && $el->isChecked(): + $this->cube->removeDimension($name); + $updateDimensions = true; + break 2; + case ($el = $this->getElement('moveDimensionUp_' . $dimensionId)) && $el->isChecked(): + $this->cube->moveDimensionUp($name); + $updateDimensions = true; + break 2; + case ($el = $this->getElement('moveDimensionDown_' . $dimensionId)) && $el->isChecked(): + $this->cube->moveDimensionDown($name); + $updateDimensions = true; + break 2; + } + } + + if ($updateDimensions) { + $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices()); + $url->setParam('dimensions', DimensionParams::update($dimensions)->getParams()); + } else { + foreach ($this->cube->listSlices() as $slice) { + $sliceId = sha1($slice); + + if (($el = $this->getElement('removeSlice_' . $sliceId)) && $el->isChecked()) { + $url->getParams()->remove(rawurlencode($slice)); + } + } + } + } + + $this->setRedirectUrl($url); + } +} diff --git a/application/views/scripts/cube-details.phtml b/application/views/scripts/cube-details.phtml new file mode 100644 index 0000000..b47bf9b --- /dev/null +++ b/application/views/scripts/cube-details.phtml @@ -0,0 +1,10 @@ +<div class="controls"> +<?= $this->tabs ?> +<h1><?= $this->escape($this->title) ?></h1> +</div> + +<div class="content"> + <ul class="action-links"> + <?= $this->links ?> + </ul> +</div> diff --git a/application/views/scripts/cube-index.phtml b/application/views/scripts/cube-index.phtml new file mode 100644 index 0000000..e45bcde --- /dev/null +++ b/application/views/scripts/cube-index.phtml @@ -0,0 +1,20 @@ +<div class="controls"> + <?php if (! $this->compact): ?> + <?= $this->tabs ?> + <h1><?= $this->escape($this->title) ?></h1> + <?php if ($this->form && ! $this->compact): ?> + <?= $this->qlink('Hide settings', $this->url()->without('showSettings'), null, ['icon' => 'wrench']) ?> + <?php else: ?> + <?= $this->qlink('Show settings', $this->url()->with('showSettings', true), null, ['icon' => 'wrench']) ?> + <?php endif ?> + <?php endif ?> +</div> + +<div class="content"> + <?php if ($this->form && ! $this->compact): ?> + <?= $this->form ?> + <?php endif ?> + <?php if ($this->cube): ?> + <?= $this->cube->render($this) ?> + <?php endif ?> +</div> diff --git a/configuration.php b/configuration.php new file mode 100644 index 0000000..eaf1130 --- /dev/null +++ b/configuration.php @@ -0,0 +1,5 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +$this->menuSection(N_('Reporting'))->add($this->translate('Cube'))->setUrl('cube/hosts')->setPriority(10); diff --git a/doc/01-About.md b/doc/01-About.md new file mode 100644 index 0000000..c07095a --- /dev/null +++ b/doc/01-About.md @@ -0,0 +1,58 @@ +# Icinga Cube + +The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icingaweb2) +module. It currently shows host and service statistics (total count, health) grouped by +various custom variables in multiple dimensions. + +![Cube - Overview](img/cube_simple.png) + +It will be your new best friend in case you are running a large environment and +want to get a quick answers to questions like: + +* Which project uses how many servers per environment at which location/site? + * Who occupies most servers? + * How many of those are used in production? + * Which project has only development and test boxes? +* Which operating system is used for which project and in which environment? + * Do we still have Debian Lenny? + * Which projects are to blame for this? + * Do we have applications where the operating systems used differ in staging + and production? +* Which project uses which operating system version for which application? + * Which projects have homogeneous environments? + * Which projects are at a consistent patch level? + * How many RHEL 6 variants (6.1, 6.2, 6.3...) do we use? + * Who is running the oldest ones? In production? +* Which projects are still using physical servers in which environment? + +For Businessmen - Drill and Slice +--------------------------------- + +Get answers to your questions. Quick and fully autonomous, using the cube +requires no technical skills. Choose amongst all available dimensions and rotate +the Cube to fit your needs. + +![Cube - Configure Dimensions](img/cube_move-up.png) + +Want to drill down? Choose a slice and get your answers: + +![Cube - Configure Dimensions](img/cube_slice.png) + +All facts configured for systems monitored by [Icinga](https://www.icinga.com/) +can be used for your research. + +For Icinga Director users +------------------------- + +![Cube - Action Links](img/cube_action-links.png) + +In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director), +in addition to the multi-selection/edit feature the cube provides a nice way to +modify multiple hosts at once. + +![Cube - Director multi-edit](img/cube_director.png) + +Installation +------------ + +To install Icinga Cube see [Installation](02-Installation.md). diff --git a/doc/02-Installation.md b/doc/02-Installation.md new file mode 100644 index 0000000..ef094bd --- /dev/null +++ b/doc/02-Installation.md @@ -0,0 +1,235 @@ +<!-- {% if index %} --> +# Installing Icinga Cube + +The recommended way to install Icinga Cube is to use prebuilt packages for +all supported platforms from our official release repository. +Please follow the steps listed for your target operating system, +which guide you through setting up the repository and installing Icinga Cube. + +<!-- {% else %} --> +<!-- {% if not from_source %} --> +## Adding Icinga Package Repository + +The recommended way to install Icinga Cube is to use prebuilt packages from our official release repository. + +!!! tip + + If you install Icinga Cube on a node that has Icinga 2, Icinga DB or Icinga Web installed via packages, + proceed to [installing the Icinga Cube package](#installing-icinga-cube-package) as + the repository is already configured. + +Here's how to add the official release repository: + +<!-- {% if amazon_linux %} --> +<!-- {% if not icingaDocs %} --> +### Amazon Linux 2 Repository +<!-- {% endif %} --> +!!! info + + A paid repository subscription is required for Amazon Linux 2 repositories. Get more information on + [icinga.com/subscription](https://icinga.com/subscription). + + Don't forget to fill in the username and password section with appropriate credentials in the local .repo file. + +```bash +rpm --import https://packages.icinga.com/icinga.key +wget https://packages.icinga.com/subscription/amazon/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo +``` +<!-- {% endif %} --> + +<!-- {% if centos %} --> +<!-- {% if not icingaDocs %} --> +### CentOS Repository +<!-- {% endif %} --> +```bash +rpm --import https://packages.icinga.com/icinga.key +wget https://packages.icinga.com/centos/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo +``` +<!-- {% endif %} --> + +<!-- {% if debian %} --> +<!-- {% if not icingaDocs %} --> +### Debian Repository +<!-- {% endif %} --> + +```bash +apt-get update +apt-get -y install apt-transport-https wget gnupg + +wget -O - https://packages.icinga.com/icinga.key | apt-key add - + +DIST=$(awk -F"[)(]+" '/VERSION=/ {print $2}' /etc/os-release); \ + echo "deb https://packages.icinga.com/debian icinga-${DIST} main" > \ + /etc/apt/sources.list.d/${DIST}-icinga.list + echo "deb-src https://packages.icinga.com/debian icinga-${DIST} main" >> \ + /etc/apt/sources.list.d/${DIST}-icinga.list + +apt-get update +``` +<!-- {% endif %} --> + +<!-- {% if rhel %} --> +<!-- {% if not icingaDocs %} --> +### RHEL Repository +<!-- {% endif %} --> +!!! info + + A paid repository subscription is required for RHEL repositories. Get more information on + [icinga.com/subscription](https://icinga.com/subscription). + + Don't forget to fill in the username and password section with appropriate credentials in the local .repo file. + +```bash +rpm --import https://packages.icinga.com/icinga.key +wget https://packages.icinga.com/subscription/rhel/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo +``` +<!-- {% endif %} --> + +<!-- {% if sles %} --> +<!-- {% if not icingaDocs %} --> +### SLES Repository +<!-- {% endif %} --> +!!! info + + A paid repository subscription is required for SLES repositories. Get more information on + [icinga.com/subscription](https://icinga.com/subscription). + + Don't forget to fill in the username and password section with appropriate credentials in the local .repo file. + +```bash +rpm --import https://packages.icinga.com/icinga.key + +zypper ar https://packages.icinga.com/subscription/sles/ICINGA-release.repo +zypper ref +``` +<!-- {% endif %} --> + +<!-- {% if ubuntu %} --> +<!-- {% if not icingaDocs %} --> +### Ubuntu Repository +<!-- {% endif %} --> + +```bash +apt-get update +apt-get -y install apt-transport-https wget gnupg + +wget -O - https://packages.icinga.com/icinga.key | apt-key add - + +. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi; \ + echo "deb https://packages.icinga.com/ubuntu icinga-${DIST} main" > \ + /etc/apt/sources.list.d/${DIST}-icinga.list + echo "deb-src https://packages.icinga.com/ubuntu icinga-${DIST} main" >> \ + /etc/apt/sources.list.d/${DIST}-icinga.list + +apt-get update +``` +<!-- {% endif %} --> + +## Installing Icinga Cube Package + +Use your distribution's package manager to install the `icinga-cube` package as follows: + +<!-- {% if amazon_linux %} --> +<!-- {% if not icingaDocs %} --> +#### Amazon Linux 2 +<!-- {% endif %} --> +```bash +yum install icinga-cube +``` +<!-- {% endif %} --> + +<!-- {% if centos %} --> +<!-- {% if not icingaDocs %} --> +#### CentOS +<!-- {% endif %} --> +!!! info + + Note that installing Icinga Cube is only supported on CentOS 7 as CentOS 8 is EOL. + +```bash +yum install icinga-cube +``` +<!-- {% endif %} --> + +<!-- {% if debian or ubuntu %} --> +<!-- {% if not icingaDocs %} --> +#### Debian / Ubuntu +<!-- {% endif %} --> +```bash +apt-get install icinga-cube +``` +<!-- {% endif %} --> + +<!-- {% if rhel %} --> +#### RHEL 8 or Later + +```bash +dnf install icinga-cube +``` + +#### RHEL 7 + +```bash +yum install icinga-cube +``` +<!-- {% endif %} --> + +<!-- {% if sles %} --> +<!-- {% if not icingaDocs %} --> +#### SLES +<!-- {% endif %} --> +```bash +zypper install icinga-cube +``` +<!-- {% endif %} --> + +<!-- {% else %} --><!-- {# end if not from_source #} --> +<!-- {% if not icingaDocs %} --> +## Installing Icinga Cube from Source +<!-- {% endif %} --> + +Please see the Icinga Web documentation on +[how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source. +Make sure you use `cube` as the module name. The following requirements must also be met. + +### Requirements + +* PHP (≥7.2) +* [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9) +* [Icinga DB Web](https://github.com/Icinga/icingadb-web) (≥1.0) +* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.9) + +If you are using PostgreSQL, you need at least 9.5 which provides the `ROLLUP` feature. +<!-- {% endif %} --><!-- {# end else if not from_source #} --> + +## Configuring Icinga Cube + +<!-- {% if not from_source %} --> +The Icinga Web PHP framework is required to configure and run the Icinga Cube. +Package installations of `icinga-cube` already set up the necessary dependencies. + +If Icinga Web has not been installed or set up before, +you have completed the instructions here and can proceed to +<!-- {% if amazon_linux %} --> +[install the web server on Amazon Linux](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/06-Amazon-Linux/#install-the-web-server), +<!-- {% endif %} --> +<!-- {% if centos %} --> +[install the web server on CentOS](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/03-CentOS/#install-the-web-server), +<!-- {% endif %} --> +<!-- {% if debian %} --> +[install the web server on Debian](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/01-Debian/#install-the-web-server), +<!-- {% endif %} --> +<!-- {% if rhel %} --> +[install the web server on RHEL](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/04-RHEL/#install-the-web-server), +<!-- {% endif %} --> +<!-- {% if sles %} --> +[install the web server on SLES](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/05-SLES/#install-the-web-server), +<!-- {% endif %} --> +<!-- {% if ubuntu %} --> +[install the web server on Ubuntu](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/02-Ubuntu/#install-the-web-server), +<!-- {% endif %} --> +which will then take you to the web-based setup wizard, which also allows you to enable the Icinga Cube. +<!-- {% endif %} --><!-- {# end if not from_source #} --> + +For Icinga Web setups already running, log in to Icinga Web with a privileged user and enable the Icinga Cube. +<!-- {% endif %} --><!-- {# end else if index #} --> diff --git a/doc/02-Installation.md.d/01-Amazon-Linux.md b/doc/02-Installation.md.d/01-Amazon-Linux.md new file mode 100644 index 0000000..30340b1 --- /dev/null +++ b/doc/02-Installation.md.d/01-Amazon-Linux.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube on Amazon Linux +<!-- {% set amazon_linux = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/02-Installation.md.d/02-CentOS.md b/doc/02-Installation.md.d/02-CentOS.md new file mode 100644 index 0000000..aaaaf1e --- /dev/null +++ b/doc/02-Installation.md.d/02-CentOS.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube on CentOS +<!-- {% set centos = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/02-Installation.md.d/03-Debian.md b/doc/02-Installation.md.d/03-Debian.md new file mode 100644 index 0000000..22c9603 --- /dev/null +++ b/doc/02-Installation.md.d/03-Debian.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube on Debian +<!-- {% set debian = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/02-Installation.md.d/04-RHEL.md b/doc/02-Installation.md.d/04-RHEL.md new file mode 100644 index 0000000..fb6f738 --- /dev/null +++ b/doc/02-Installation.md.d/04-RHEL.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube on RHEL +<!-- {% set rhel = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/02-Installation.md.d/05-SLES.md b/doc/02-Installation.md.d/05-SLES.md new file mode 100644 index 0000000..eb7bba7 --- /dev/null +++ b/doc/02-Installation.md.d/05-SLES.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube on SLES +<!-- {% set sles = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/02-Installation.md.d/06-Ubuntu.md b/doc/02-Installation.md.d/06-Ubuntu.md new file mode 100644 index 0000000..0a4cbf2 --- /dev/null +++ b/doc/02-Installation.md.d/06-Ubuntu.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube on Ubuntu +<!-- {% set ubuntu = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/02-Installation.md.d/07-From-Source.md b/doc/02-Installation.md.d/07-From-Source.md new file mode 100644 index 0000000..a5cb8d1 --- /dev/null +++ b/doc/02-Installation.md.d/07-From-Source.md @@ -0,0 +1,3 @@ +# Installing Icinga Cube from Source +<!-- {% set from_source = True %} --> +<!-- {% include "02-Installation.md" %} --> diff --git a/doc/img/cube_action-links.png b/doc/img/cube_action-links.png Binary files differnew file mode 100644 index 0000000..79542e9 --- /dev/null +++ b/doc/img/cube_action-links.png diff --git a/doc/img/cube_director.png b/doc/img/cube_director.png Binary files differnew file mode 100644 index 0000000..3fc3730 --- /dev/null +++ b/doc/img/cube_director.png diff --git a/doc/img/cube_move-up.png b/doc/img/cube_move-up.png Binary files differnew file mode 100644 index 0000000..980a503 --- /dev/null +++ b/doc/img/cube_move-up.png diff --git a/doc/img/cube_simple.png b/doc/img/cube_simple.png Binary files differnew file mode 100644 index 0000000..0690d59 --- /dev/null +++ b/doc/img/cube_simple.png diff --git a/doc/img/cube_slice.png b/doc/img/cube_slice.png Binary files differnew file mode 100644 index 0000000..3aff4db --- /dev/null +++ b/doc/img/cube_slice.png diff --git a/library/Cube/Cube.php b/library/Cube/Cube.php new file mode 100644 index 0000000..b307869 --- /dev/null +++ b/library/Cube/Cube.php @@ -0,0 +1,323 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Exception\IcingaException; +use Icinga\Web\View; + +abstract class Cube +{ + /** @var array<string, Dimension> Available dimensions */ + protected $availableDimensions; + + /** @var array Fact names */ + protected $chosenFacts; + + /** @var Dimension[] */ + protected $dimensions = array(); + + protected $slices = array(); + + protected $renderer; + + abstract public function fetchAll(); + + public function removeDimension($name) + { + unset($this->dimensions[$name]); + unset($this->slices[$name]); + return $this; + } + + /** + * @return CubeRenderer + * @throws IcingaException + */ + public function getRenderer() + { + throw new IcingaException('Got no cube renderer'); + } + + public function getPathLabel() + { + $dimensions = $this->getDimensionsLabel(); + $slices = $this->getSlicesLabel(); + $parts = array(); + if ($dimensions !== null) { + $parts[] = $dimensions; + } + + if ($slices !== null) { + $parts[] = $slices; + } + + return implode(', ', $parts); + } + + public function getDimensionsLabel() + { + $dimensions = $this->listDimensions(); + if (empty($dimensions)) { + return null; + } + + return implode(' -> ', array_map(function ($d) { + return $d->getLabel(); + }, $dimensions)); + } + + public function getSlicesLabel() + { + $parts = array(); + + $slices = $this->getSlices(); + if (empty($slices)) { + return null; + } + foreach ($slices as $key => $value) { + $parts[] = sprintf('%s = %s', $this->getDimension($key)->getLabel(), $value); + } + + return implode(', ', $parts); + } + + /** + * Create a new dimension + * + * @param string $name + * @return Dimension + */ + abstract public function createDimension($name); + + protected function registerAvailableDimensions() + { + if ($this->availableDimensions !== null) { + return; + } + + $this->availableDimensions = []; + foreach ($this->listAvailableDimensions() as $label) { + $name = strtolower($label); + if (! isset($this->availableDimensions[$name])) { + $this->availableDimensions[$name] = $this->createDimension($name)->setLabel($label); + } else { + $this->availableDimensions[$name]->addLabel($label); + } + } + } + + public function listAdditionalDimensions() + { + $this->registerAvailableDimensions(); + + $list = []; + foreach ($this->availableDimensions as $name => $dimension) { + if (! $this->hasDimension($name)) { + $list[$name] = $dimension->getLabel(); + } + } + + return $list; + } + + abstract public function listAvailableDimensions(); + + public function getDimensionAfter($name) + { + $found = false; + $after = null; + + foreach ($this->listDimensions() as $k => $d) { + if ($found) { + $after = $d; + break; + } + + if ($k === $name) { + $found = true; + } + } + + return $after; + } + + public function listDimensionsUpTo($name) + { + $res = array(); + foreach ($this->listDimensions() as $d => $_) { + $res[] = $d; + if ($d === $name) { + break; + } + } + + return $res; + } + + public function moveDimensionUp($name) + { + $last = $found = null; + $positions = array_keys($this->dimensions); + + foreach ($positions as $k => $v) { + if ($v === $name) { + $found = $k; + break; + } + + $last = $k; + } + + if ($found !== null) { + $this->flipPositions($positions, $last, $found); + } + + $this->reOrderDimensions($positions); + return $this; + } + + public function moveDimensionDown($name) + { + $next = $found = null; + $positions = array_keys($this->dimensions); + + foreach ($positions as $k => $v) { + if ($found !== null) { + $next = $k; + break; + } + + if ($v === $name) { + $found = $k; + } + } + + if ($next !== null) { + $this->flipPositions($positions, $next, $found); + } + + $this->reOrderDimensions($positions); + return $this; + } + + protected function flipPositions(&$array, $pos1, $pos2) + { + list( + $array[$pos1], + $array[$pos2] + ) = array( + $array[$pos2], + $array[$pos1] + ); + } + + protected function reOrderDimensions($positions) + { + $dimensions = array(); + foreach ($positions as $pos => $key) { + $dimensions[$key] = $this->dimensions[$key]; + } + + $this->dimensions = $dimensions; + } + + public function addDimension(Dimension $dimension) + { + $name = $dimension->getName(); + if (array_key_exists($name, $this->dimensions)) { + throw new IcingaException('Cannot add dimension "%s" twice', $name); + } + + $this->dimensions[$name] = $dimension; + return $this; + } + + public function slice($key, $value) + { + if ($this->hasDimension($key)) { + $this->slices[$key] = $value; + } else { + throw new IcingaException('Got no such dimension: "%s"', $key); + } + + return $this; + } + + public function hasDimension($name) + { + return array_key_exists($name, $this->dimensions); + } + + public function hasSlice($name) + { + return array_key_exists($name, $this->slices); + } + + public function listSlices() + { + return array_keys($this->slices); + } + + public function getSlices() + { + return $this->slices; + } + + public function hasFact($name) + { + return array_key_exists($name, $this->chosenFacts); + } + + public function getDimension($name) + { + return $this->dimensions[$name]; + } + + /** + * Return a list of chosen facts + * + * @return array + */ + public function listFacts() + { + return $this->chosenFacts; + } + + /** + * Choose a list of facts + * + * @param array $facts + * @return $this + */ + public function chooseFacts(array $facts) + { + $this->chosenFacts = $facts; + return $this; + } + + public function listDimensions() + { + return array_diff_key($this->dimensions, $this->slices); + } + + public function listColumns() + { + return array_merge(array_keys($this->listDimensions()), $this->listFacts()); + } + + /** + * @param View $view + * @param CubeRenderer $renderer + * @return string + */ + public function render(View $view, CubeRenderer $renderer = null) + { + if ($renderer === null) { + $renderer = $this->getRenderer(); + } + + return $renderer->render($view); + } +} diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php new file mode 100644 index 0000000..3f8c80d --- /dev/null +++ b/library/Cube/CubeRenderer.php @@ -0,0 +1,406 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Web\View; + +/** + * CubeRenderer base class + * + * Every Cube Renderer must extend this class. + * + * TODO: Should we introduce DimensionRenderer, FactRenderer and SummaryHelper + * instead? + * + * @package Icinga\Module\Cube + */ +abstract class CubeRenderer +{ + /** @var View */ + protected $view; + + /** @var Cube */ + protected $cube; + + /** @var array Our dimensions */ + protected $dimensions; + + /** @var array Our dimensions in regular order */ + protected $dimensionOrder; + + /** @var array Our dimensions in reversed order as a quick lookup source */ + protected $reversedDimensions; + + /** @var array Level (deepness) for each dimension (0, 1, 2...) */ + protected $dimensionLevels; + + protected $facts; + + /** @var object The row before the current one */ + protected $lastRow; + + /** + * Current summaries + * + * This is an object of objects, with dimension names being the keys and + * a facts row containing current (rollup) summaries for that dimension + * being it's value + * + * @var object + */ + protected $summaries; + + protected $started; + + /** + * CubeRenderer constructor. + * + * @param Cube $cube + */ + public function __construct(Cube $cube) + { + $this->cube = $cube; + } + + /** + * Render the given facts + * + * @param $facts + * @return string + */ + abstract public function renderFacts($facts); + + /** + * Returns the base url for the details action + * + * @return string + */ + abstract protected function getDetailsBaseUrl(); + + /** + * Initialize all we need + */ + protected function initialize() + { + $this->started = false; + $this->initializeDimensions() + ->initializeFacts() + ->initializeLastRow() + ->initializeSummaries(); + } + + /** + * @return $this + */ + protected function initializeLastRow() + { + $object = (object) array(); + foreach ($this->dimensions as $dimension) { + $object->{$dimension->getName()} = null; + } + + $this->lastRow = $object; + + return $this; + } + + /** + * @return $this + */ + protected function initializeDimensions() + { + $this->dimensions = $this->cube->listDimensions(); + + $min = 3; + $cnt = count($this->dimensions); + if ($cnt < $min) { + $pos = 0; + $diff = $min - $cnt; + $this->dimensionOrder = []; + foreach ($this->dimensions as $name => $_) { + $this->dimensionOrder[$pos++ + $diff] = $name; + } + } else { + $this->dimensionOrder = array_keys($this->dimensions); + } + + $this->reversedDimensions = array_reverse($this->dimensionOrder); + $this->dimensionLevels = array_flip($this->dimensionOrder); + return $this; + } + + /** + * @return $this + */ + protected function initializeFacts() + { + $this->facts = $this->cube->listFacts(); + return $this; + } + + /** + * @return $this + */ + protected function initializeSummaries() + { + $this->summaries = (object) array(); + return $this; + } + + /** + * @param object $row + * @return bool + */ + protected function startsDimension($row) + { + foreach ($this->dimensionOrder as $name) { + if ($row->$name === null) { + $this->summaries->$name = $this->extractFacts($row); + return true; + } + } + + return false; + } + + /** + * @param $row + * @return object + */ + protected function extractFacts($row) + { + $res = (object) array(); + + foreach ($this->facts as $fact) { + $res->$fact = $row->$fact; + } + + return $res; + } + + public function render(View $view) + { + $this->view = $view; + $this->initialize(); + $htm = $this->beginContainer(); + foreach ($this->cube->fetchAll() as $row) { + $htm .= $this->renderRow($row); + } + + return $htm . $this->closeDimensions() . $this->endContainer(); + } + + protected function renderRow($row) + { + $htm = ''; + if ($dimension = $this->startsDimension($row)) { + return $htm; + } + + $htm .= $this->closeDimensionsForRow($row); + $htm .= $this->beginDimensionsForRow($row); + $htm .= $this->renderFacts($row); + $this->lastRow = $row; + return $htm; + } + + protected function beginDimensionsForRow($row) + { + $last = $this->lastRow; + foreach ($this->dimensionOrder as $name) { + if ($last->$name !== $row->$name) { + return $this->beginDimensionsUpFrom($name, $row); + } + } + + return ''; + } + + protected function beginDimensionsUpFrom($dimension, $row) + { + $htm = ''; + $found = false; + + foreach ($this->dimensionOrder as $name) { + if ($name === $dimension) { + $found = true; + } + + if ($found) { + $htm .= $this->beginDimension($name, $row); + } + } + + return $htm; + } + + protected function closeDimensionsForRow($row) + { + $last = $this->lastRow; + foreach ($this->dimensionOrder as $name) { + if ($last->$name !== $row->$name) { + return $this->closeDimensionsDownTo($name); + } + } + + return ''; + } + + protected function closeDimensionsDownTo($name) + { + $htm = ''; + + foreach ($this->reversedDimensions as $dimension) { + $htm .= $this->closeDimension($dimension); + + if ($name === $dimension) { + break; + } + } + + return $htm; + } + + protected function closeDimensions() + { + $htm = ''; + foreach ($this->reversedDimensions as $name) { + $htm .= $this->closeDimension($name); + } + + return $htm; + } + + protected function closeDimension($name) + { + if (! $this->started) { + return ''; + } + + $indent = $this->getIndent($name); + return $indent . ' </div>' . "\n" . $indent . "</div><!-- $name -->\n"; + } + + protected function getIndent($name) + { + return str_repeat(' ', $this->getLevel($name)); + } + + protected function beginDimension($name, $row) + { + $indent = $this->getIndent($name); + if (! $this->started) { + $this->started = true; + } + $view = $this->view; + $dimension = $this->cube->getDimension($name); + + return + $indent . '<div class="' + . $this->getDimensionClassString($name, $row) + . '">' . "\n" + . $indent . ' <div class="header"><a href="' + . $this->getDetailsUrl($name, $row) + . '" title="' . $view->escape(sprintf('Show details for %s: %s', $dimension->getLabel(), $row->$name)) . '"' + . ' data-base-target="_next">' + . $this->renderDimensionLabel($name, $row) + . '</a><a class="icon-filter" href="' + . $this->getSliceUrl($name, $row) + . '" title="' . $view->escape('Slice this cube') . '"></a></div>' . "\n" + . $indent . ' <div class="body">' . "\n"; + } + + /** + * Render the label for a given dimension name + * + * To have some context available, also + * + * @param $name + * @param $row + * @return string + */ + protected function renderDimensionLabel($name, $row) + { + $caption = $row->$name; + if (empty($caption)) { + $caption = '_'; + } + + return $this->view->escape($caption); + } + + protected function getDetailsUrl($name, $row) + { + $cube = $this->cube; + + $dimensions = array_merge(array_keys($cube->listDimensions()), $cube->listSlices()); + $params = [ + 'dimensions' => DimensionParams::update($dimensions)->getParams() + ]; + + foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) { + $params[$dimensionName] = $row->$dimensionName; + } + + foreach ($this->cube->getSlices() as $key => $val) { + $params[$key] = $val; + } + + return $this->view->url( + $this->getDetailsBaseUrl(), + $params + ); + } + + protected function getSliceUrl($name, $row) + { + return $this->view->url() + ->setParam($name, $row->$name); + } + + protected function isOuterDimension($name) + { + return $this->reversedDimensions[0] !== $name; + } + + protected function getDimensionClassString($name, $row) + { + return implode(' ', $this->getDimensionClasses($name, $row)); + } + + protected function getDimensionClasses($name, $row) + { + return array('cube-dimension' . $this->getLevel($name)); + } + + protected function getLevel($name) + { + return $this->dimensionLevels[$name]; + } + + /** + * @return string + */ + protected function beginContainer() + { + return '<div class="cube">' . "\n"; + } + + /** + * @return string + */ + protected function endContainer() + { + return '</div>' . "\n"; + } + + /** + * Well... just to be on the safe side + */ + public function __destruct() + { + unset($this->cube); + } +} diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php new file mode 100644 index 0000000..89322cb --- /dev/null +++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php @@ -0,0 +1,105 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\CubeRenderer; + +use Icinga\Module\Cube\CubeRenderer; + +class HostStatusCubeRenderer extends CubeRenderer +{ + protected function renderDimensionLabel($name, $row) + { + $htm = parent::renderDimensionLabel($name, $row); + + if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) { + $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->hosts_cnt . ')</span>'; + } + + return $htm; + } + + protected function getDimensionClasses($name, $row) + { + $classes = parent::getDimensionClasses($name, $row); + + $sums = $row; + if ($sums->hosts_down > 0) { + $classes[] = 'critical'; + if ((int) $sums->hosts_unhandled_down === 0) { + $classes[] = 'handled'; + } + } elseif ($sums->hosts_unreachable > 0) { + $classes[] = 'unreachable'; + if ((int) $sums->hosts_unhandled_unreachable === 0) { + $classes[] = 'handled'; + } + } else { + $classes[] = 'ok'; + } + + return $classes; + } + + public function renderFacts($facts) + { + $indent = str_repeat(' ', 3); + $parts = array(); + + if ($facts->hosts_unhandled_down > 0) { + $parts['critical'] = $facts->hosts_unhandled_down; + } + + if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) { + $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down; + } + + if ($facts->hosts_unhandled_unreachable > 0) { + $parts['unreachable'] = $facts->hosts_unhandled_unreachable; + } + + if ($facts->hosts_unreachable > 0 && $facts->hosts_unreachable > $facts->hosts_unhandled_unreachable) { + $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable; + } + + if ($facts->hosts_cnt > $facts->hosts_down && $facts->hosts_cnt > $facts->hosts_unreachable) { + $parts['ok'] = $facts->hosts_cnt - $facts->hosts_down - $facts->hosts_unreachable; + } + + $main = ''; + $sub = ''; + foreach ($parts as $class => $count) { + if ($main === '') { + $main = $this->makeBadgeHtml($class, $count); + } else { + $sub .= $this->makeBadgeHtml($class, $count); + } + } + if ($sub !== '') { + $sub = $indent + . '<span class="others">' + . "\n " + . $sub + . $indent + . "</span>\n"; + } + + return $main . $sub; + } + + protected function makeBadgeHtml($class, $count) + { + $indent = str_repeat(' ', 3); + return sprintf( + '%s<span class="%s">%s</span>', + $indent, + $class, + $count + ) . "\n"; + } + + protected function getDetailsBaseUrl() + { + return 'cube/hosts/details'; + } +} diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php new file mode 100644 index 0000000..33caa84 --- /dev/null +++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php @@ -0,0 +1,126 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\CubeRenderer; + +use Icinga\Module\Cube\CubeRenderer; + +class ServiceStatusCubeRenderer extends CubeRenderer +{ + public function renderFacts($facts) + { + $indent = str_repeat(' ', 3); + $parts = []; + + if ($facts->services_unhandled_critical > 0) { + $parts['critical'] = $facts->services_unhandled_critical; + } + + if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) { + $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical; + } + + if ($facts->services_unhandled_warning > 0) { + $parts['warning'] = $facts->services_unhandled_warning; + } + + if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) { + $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning; + } + + if ($facts->services_unhandled_unknown > 0) { + $parts['unknown'] = $facts->services_unhandled_unknown; + } + + if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) { + $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown; + } + + if ( + $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning + && $facts->services_cnt > $facts->services_unknown + ) { + $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning - + $facts->services_unknown; + } + + $main = ''; + $sub = ''; + foreach ($parts as $class => $count) { + if ($main === '') { + $main = $this->makeBadgeHtml($class, $count); + } else { + $sub .= $this->makeBadgeHtml($class, $count); + } + } + if ($sub !== '') { + $sub = $indent + . '<span class="others">' + . "\n " + . $sub + . $indent + . "</span>\n"; + } + + return $main . $sub; + } + + /** + * @inheritdoc + */ + protected function renderDimensionLabel($name, $row) + { + $htm = parent::renderDimensionLabel($name, $row); + + if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) { + $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->services_cnt . ')</span>'; + } + + return $htm; + } + + protected function getDimensionClasses($name, $row) + { + $classes = parent::getDimensionClasses($name, $row); + + $sums = $row; + if ($sums->services_critical > 0) { + $classes[] = 'critical'; + if ((int) $sums->services_unhandled_critical === 0) { + $classes[] = 'handled'; + } + } elseif ($sums->services_warning > 0) { + $classes[] = 'warning'; + if ((int) $sums->services_unhandled_warning === 0) { + $classes[] = 'handled'; + } + } elseif ($sums->services_unknown > 0) { + $classes[] = 'unknown'; + if ((int) $sums->services_unhandled_unknown === 0) { + $classes[] = 'handled'; + } + } else { + $classes[] = 'ok'; + } + + return $classes; + } + + protected function makeBadgeHtml($class, $count) + { + $indent = str_repeat(' ', 3); + + return sprintf( + '%s<span class="%s">%s</span>', + $indent, + $class, + $count + ) . "\n"; + } + + protected function getDetailsBaseUrl() + { + return 'cube/services/details'; + } +} diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php new file mode 100644 index 0000000..7519da4 --- /dev/null +++ b/library/Cube/Dimension.php @@ -0,0 +1,52 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +/** + * Dimension interface + * + * All available dimensions must implement this interface + * + * @package Icinga\Module\Cube + */ +interface Dimension +{ + /** + * The name of this dimension + * + * @return string + */ + public function getName(); + + /** + * The label of this dimension + * + * @return string + */ + public function getLabel(); + + /** + * Column expression + * + * This is the expression used to fetch the related column. Usually an SQL + * snippet when a relational database is involved + * + * @param Cube $cube + * + * @return string + */ + public function getColumnExpression(Cube $cube); + + /** + * Add this dimension to a cube + * + * This allows your dimension to apply itself to the Cube. That way your + * dimension is able to join optional tables and more + * + * @param Cube $cube + * @return void + */ + public function addToCube(Cube $cube); +} diff --git a/library/Cube/DimensionParams.php b/library/Cube/DimensionParams.php new file mode 100644 index 0000000..02b7ea1 --- /dev/null +++ b/library/Cube/DimensionParams.php @@ -0,0 +1,87 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Web\Url; + +class DimensionParams +{ + /** + * @var array Raw dimensions + */ + protected $dimensions = []; + + /** + * @var string encoded dimensions separated by coma + */ + protected $params; + + // For the form: DimensionsParam::fromUrl($url) + public static function fromUrl(Url $url) + { + return static::fromString($url->getParam('dimensions')); + } + + public static function fromArray(array $dimensions = []) + { + $self = new static(); + + $self->dimensions = array_filter($dimensions); + + return $self; + } + + // For the controller: DimensionsParam::fromArray($this->params->shift('dimensions')) + public static function fromString($dimensions) + { + return static::fromArray(explode(',', $dimensions)); + } + + + /** + * @param $dimension + * + * @return $this + */ + public function add($dimension) + { + if (! empty($dimension)) { + $this->dimensions[] = $dimension; + } + + return $this; + } + + /** + * Overwrite dimensions + * + * @param $dimensions + * + * @return $this + */ + public static function update($dimensions) + { + $self = new static(); + $self->dimensions = $dimensions; + + return $self; + } + + /** + * @return string encoded dimensions separated by coma + */ + public function getParams() + { + return implode(',', $this->dimensions); + } + + /** + * @return array + */ + public function getDimensions() + { + return $this->dimensions; + } +} diff --git a/library/Cube/Hook/ActionsHook.php b/library/Cube/Hook/ActionsHook.php new file mode 100644 index 0000000..8ba8a7c --- /dev/null +++ b/library/Cube/Hook/ActionsHook.php @@ -0,0 +1,99 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Hook; + +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Web\ActionLink; +use Icinga\Module\Cube\Web\ActionLinks; +use Icinga\Web\Url; +use Icinga\Web\View; + +/** + * ActionsHook + * + * Implement this hook in case your module wants to add links to the detail + * page shown for a slice. + * + * @package Icinga\Module\Cube\Hook + */ +abstract class ActionsHook +{ + /** @var ActionLinks */ + private $actionLinks; + + /** + * Your implementation should extend this method + * + * Then use the addActionLink() method, eventually combined with the + * createUrl() helper like this: + * + * <code> + * $this->addActionLink( + * $this->makeUrl('mymodule/controller/action', array('some' => 'param')), + * 'A shown title', + * 'A longer description text, should fit into the available square field', + * 'icon-name' + * ); + * </code> + * + * For a list of available icon names please enable the Icinga Web 2 'doc' + * module and go to "Documentation" -> "Developer - Style" -> "Icons" + * + * @param Cube $cube + * @param View $view + * + * @return void + */ + abstract public function prepareActionLinks(Cube $cube, View $view); + + /** + * Lazy access to an ActionLinks object + * + * @return ActionLinks + */ + public function getActionLinks() + { + if ($this->actionLinks === null) { + $this->actionLinks = new ActionLinks(); + } + return $this->actionLinks; + } + + /** + * Helper method instantiating an ActionLink object + * + * @param Url $url + * @param string $title + * @param string $description + * @param string $icon + * + * @return $this + */ + public function addActionLink(Url $url, $title, $description, $icon) + { + $this->getActionLinks()->add( + new ActionLink($url, $title, $description, $icon) + ); + + return $this; + } + + /** + * Helper method instantiating an Url object + * + * @param string $path + * @param array $params + * @return Url + */ + public function makeUrl($path, $params = null) + { + $url = Url::fromPath($path); + if ($params !== null) { + $url->getParams()->mergeValues($params); + } + + return $url; + } +} diff --git a/library/Cube/Hook/IcingaDbActionsHook.php b/library/Cube/Hook/IcingaDbActionsHook.php new file mode 100644 index 0000000..63c24fe --- /dev/null +++ b/library/Cube/Hook/IcingaDbActionsHook.php @@ -0,0 +1,125 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Hook; + +use Exception; +use Icinga\Application\Hook; +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use ipl\Web\Url; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Web\Widget\Icon; +use ipl\Web\Widget\Link; + +/** + * ActionsHook + * + * Implement this hook in case your module wants to add links to the detail + * page shown for a slice. + * + * @package Icinga\Module\Cube\Hook + */ +abstract class IcingaDbActionsHook +{ + /** @var Link[] */ + private $actionLinks = []; + + /** + * Create additional action links for the given cube + * + * @param IcingaDbCube $cube + * @return void + */ + abstract public function createActionLinks(IcingaDbCube $cube); + + /** + * Return the action links for the cube + * + * @return Link[] + */ + final protected function getActionLinks(): array + { + return $this->actionLinks; + } + + /** + * Helper method to populate action links array + * + * @param Url $url + * @param string $title + * @param string $description + * @param string $icon + * + * @return $this + */ + final protected function addActionLink(Url $url, string $title, string $description, string $icon): self + { + $linkContent = (new HtmlDocument()); + $linkContent->addHtml(new Icon($icon)); + $linkContent->addHtml(HtmlElement::create('span', ['class' => 'title'], $title)); + $linkContent->addHtml(HtmlElement::create('p', null, $description)); + + $this->actionLinks[] = new Link($linkContent, $url); + + return $this; + } + + /** + * Helper method instantiating an Url object + * + * @param string $path + * @param array $params + * @return Url + */ + final protected function makeUrl(string $path, array $params = null): Url + { + $url = Url::fromPath($path); + if ($params !== null) { + $url->getParams()->mergeValues($params); + } + + return $url; + } + + /** + * Render all links for all Hook implementations + * + * This is what the Cube calls when rendering details + * + * @param IcingaDbCube $cube + * + * @return string + */ + public static function renderAll(Cube $cube) + { + $html = new HtmlDocument(); + + /** @var IcingaDbActionsHook $hook */ + foreach (Hook::all('Cube/IcingaDbActions') as $hook) { + try { + $hook->createActionLinks($cube); + } catch (Exception $e) { + $html->addHtml(HtmlElement::create('li', ['class' => 'error'], $e->getMessage())); + } + + foreach ($hook->getActionLinks() as $link) { + $html->addHtml(HtmlElement::create('li', null, $link)); + } + } + + if ($html->isEmpty()) { + $html->addHtml( + HtmlElement::create( + 'li', + ['class' => 'error'], + t('No action links have been provided for this cube') + ) + ); + } + + return $html->render(); + } +} diff --git a/library/Cube/IcingaDb/CustomVariableDimension.php b/library/Cube/IcingaDb/CustomVariableDimension.php new file mode 100644 index 0000000..2266395 --- /dev/null +++ b/library/Cube/IcingaDb/CustomVariableDimension.php @@ -0,0 +1,119 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\IcingaDb; + +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Dimension; +use Icinga\Module\Icingadb\Model\CustomvarFlat; +use ipl\Sql\Expression; +use ipl\Stdlib\Filter; + +class CustomVariableDimension implements Dimension +{ + protected $name; + + protected $label; + + protected $wantNull = false; + + public function __construct($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getLabel() + { + return $this->label ?: $this->getName(); + } + + /** + * Set the label + * + * @param string $label + * @return $this + */ + public function setLabel($label) + { + $this->label = $label; + + return $this; + } + + /** + * Add a label + * + * @param string $label + * @return $this + */ + public function addLabel($label) + { + if ($this->label === null) { + $this->setLabel($label); + } else { + $this->label .= ' & ' . $label; + } + + return $this; + } + + /** + * Define whether null values should be shown + * + * @param bool $wantNull + * @return $this + */ + public function wantNull($wantNull = true) + { + $this->wantNull = $wantNull; + + return $this; + } + + /** + * @param IcingaDbCube $cube + * @return Expression|string + */ + public function getColumnExpression(Cube $cube) + { + $expression = $cube->getDb()->quoteIdentifier(['c_' . $this->getName(), 'flatvalue']); + + if ($this->wantNull) { + return new Expression("COALESCE($expression, '-')"); + } + + return $expression; + } + + public function addToCube(Cube $cube) + { + /** @var IcingaDbCube $cube */ + $name = $this->getName(); + $innerQuery = $cube->innerQuery(); + $sourceTable = $innerQuery->getModel()->getTableName(); + + $subQuery = $innerQuery->createSubQuery(new CustomvarFlat(), $sourceTable . '.vars'); + $subQuery->getSelectBase()->resetWhere(); // The link to the outer query is the ON condition + $subQuery->columns(['flatvalue', 'object_id' => $sourceTable . '.id']); + $subQuery->filter(Filter::like('flatname', $name)); + + // Values might not be unique (wildcard dimensions) + $subQuery->getSelectBase()->groupBy([ + $subQuery->getResolver()->getAlias($subQuery->getModel()) . '.flatvalue', + 'object_id' + ]); + + $subQueryAlias = $cube->getDb()->quoteIdentifier(['c_' . $name]); + $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue'); + $innerQuery->getSelectBase()->join( + [$subQueryAlias => $subQuery->assembleSelect()], + [$subQueryAlias . '.object_id = ' . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . '.id'] + ); + } +} diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php new file mode 100644 index 0000000..3f38d9b --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbCube.php @@ -0,0 +1,196 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\IcingaDb; + +use Icinga\Module\Cube\Cube; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use ipl\Orm\Query; +use ipl\Sql\Adapter\Pgsql; +use ipl\Sql\Expression; +use ipl\Sql\Select; + +abstract class IcingaDbCube extends Cube +{ + use Auth; + use Database; + + /** @var Query The inner query fetching all required data */ + protected $innerQuery; + + /** @var Select The rollup query, creating grouped sums over innerQuery */ + protected $rollupQuery; + + /** @var Select The outer query, orders respecting NULL values, rollup first */ + protected $fullQuery; + + protected $objectsFilter; + + abstract public function getObjectsFilter(); + /** + * An IcingaDbCube must provide a list of all available columns + * + * This is a key/value array with the key being the fact name / column alias + * and + * + * @return array + */ + abstract public function getAvailableFactColumns(); + + /** + * @return Query + */ + abstract public function prepareInnerQuery(); + + /** + * Get our inner query + * + * Hint: mostly used to get rid of NULL values + * + * @return Query + */ + public function innerQuery() + { + if ($this->innerQuery === null) { + $this->innerQuery = $this->prepareInnerQuery(); + } + + return $this->innerQuery; + } + + /** + * Get our rollup query + * + * @return Select + */ + protected function rollupQuery() + { + if ($this->rollupQuery === null) { + $this->rollupQuery = $this->prepareRollupQuery(); + } + + return $this->rollupQuery; + } + + /** + * Add a specific named dimension + * + * @param string $name + * @return $this + */ + public function addDimensionByName($name) + { + $this->addDimension($this->createDimension($name)); + + return $this; + } + + /** + * We first prepare the queries and to finalize it later on + * + * This way dimensions can be added one by one, they will be allowed to + * optionally join additional tables or apply other modifications late + * in the process + * + * @return void + */ + protected function finalizeInnerQuery() + { + $query = $this->innerQuery()->getSelectBase(); + $columns = []; + foreach ($this->dimensions as $name => $dimension) { + $quotedDimension = $this->getDb()->quoteIdentifier([$name]); + $dimension->addToCube($this); + $columns[$quotedDimension] = $dimension->getColumnExpression($this); + + if ($this->hasSlice($name)) { + $query->where( + $dimension->getColumnExpression($this) . ' = ?', + $this->slices[$name] + ); + } else { + $columns[$quotedDimension] = $dimension->getColumnExpression($this); + } + } + + $query->columns($columns); + } + + protected function prepareRollupQuery() + { + $dimensions = $this->listDimensions(); + $this->finalizeInnerQuery(); + + $columns = []; + $groupBy = []; + foreach ($dimensions as $name => $dimension) { + $quotedDimension = $this->getDb()->quoteIdentifier([$name]); + + $columns[$quotedDimension] = 'f.' . $quotedDimension; + $groupBy[] = $quotedDimension; + } + + $availableFacts = $this->getAvailableFactColumns(); + + foreach ($this->chosenFacts as $alias) { + $columns[$alias] = new Expression('SUM(f.' . $availableFacts[$alias] . ')'); + } + + if (! empty($groupBy)) { + if ($this->getDb()->getAdapter() instanceof Pgsql) { + $groupBy = 'ROLLUP(' . implode(', ', $groupBy) . ')'; + } else { + $groupBy[count($groupBy) - 1] .= ' WITH ROLLUP'; + } + } + + $rollupQuery = new Select(); + $rollupQuery->from(['f' => $this->innerQuery()->assembleSelect()]) + ->columns($columns) + ->groupBy($groupBy); + + return $rollupQuery; + } + + protected function prepareFullQuery() + { + $rollupQuery = $this->rollupQuery(); + $columns = []; + foreach ($this->listColumns() as $column) { + $quotedColumn = $this->getDb()->quoteIdentifier([$column]); + $columns[$quotedColumn] = 'rollup.' . $this->getDb()->quoteIdentifier([$column]); + } + + $fullQuery = new Select(); + $fullQuery->from(['rollup' => $rollupQuery])->columns($columns); + + foreach ($columns as $quotedColumn => $_) { + $fullQuery->orderBy("($quotedColumn IS NOT NULL)"); + $fullQuery->orderBy($quotedColumn); + } + + return $fullQuery; + } + + /** + * Lazy-load our full query + * + * @return Select + */ + protected function fullQuery() + { + if ($this->fullQuery === null) { + $this->fullQuery = $this->prepareFullQuery(); + } + + return $this->fullQuery; + } + + public function fetchAll() + { + $query = $this->fullQuery(); + return $this->getDb()->fetchAll($query); + } +} diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php new file mode 100644 index 0000000..028d4d7 --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php @@ -0,0 +1,100 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\IcingaDb; + +use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer; +use Icinga\Module\Icingadb\Model\CustomvarFlat; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\HoststateSummary; +use ipl\Stdlib\Filter; + +class IcingaDbHostStatusCube extends IcingaDbCube +{ + public function getRenderer() + { + return new HostStatusCubeRenderer($this); + } + + public function getAvailableFactColumns() + { + return [ + 'hosts_cnt' => 'hosts_total', + 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled', + 'hosts_unhandled_down' => 'hosts_down_unhandled', + 'hosts_unreachable' => 'hosts_unreachable', + 'hosts_unhandled_unreachable' => 'hosts_unreachable_unhandled' + ]; + } + + public function createDimension($name) + { + $this->registerAvailableDimensions(); + + if (isset($this->availableDimensions[$name])) { + return clone $this->availableDimensions[$name]; + } + + return new CustomVariableDimension($name); + } + + public function listAvailableDimensions() + { + $db = $this->getDb(); + + $query = CustomvarFlat::on($db); + $this->applyRestrictions($query); + + $query + ->columns('flatname') + ->orderBy('flatname') + ->filter(Filter::like('host.id', '*')); + $query->getSelectBase()->groupBy('flatname'); + + $dimensions = []; + foreach ($query as $row) { + // Replaces array index notations with [*] to get results for arbitrary indexes + $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->flatname); + $dimensions[$name] = $name; + } + + return $dimensions; + } + + public function prepareInnerQuery() + { + $query = HoststateSummary::on($this->getDb()); + $query->columns(array_diff_key($query->getModel()->getColumns(), (new Host())->getColumns())); + $query->disableDefaultSort(); + $this->applyRestrictions($query); + + $this->innerQuery = $query; + return $this->innerQuery; + } + + /** + * Return Filter for Hosts cube. + * + * @return Filter\Any|Filter\Chain + */ + public function getObjectsFilter() + { + if ($this->objectsFilter === null) { + $this->finalizeInnerQuery(); + + $hosts = $this->innerQuery()->columns(['host' => 'host.name']); + $hosts->getSelectBase()->resetGroupBy(); + + $filter = Filter::any(); + + foreach ($hosts as $object) { + $filter->add(Filter::equal('host.name', $object->host)); + } + + $this->objectsFilter = $filter; + } + + return $this->objectsFilter; + } +} diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php new file mode 100644 index 0000000..9336cdf --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php @@ -0,0 +1,109 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\IcingaDb; + +use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer; +use Icinga\Module\Icingadb\Model\CustomvarFlat; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Model\ServicestateSummary; +use ipl\Stdlib\Filter; + +class IcingaDbServiceStatusCube extends IcingaDbCube +{ + public function getRenderer() + { + return new ServiceStatusCubeRenderer($this); + } + + public function createDimension($name) + { + $this->registerAvailableDimensions(); + + if (isset($this->availableDimensions[$name])) { + return clone $this->availableDimensions[$name]; + } + + return new CustomVariableDimension($name); + } + + public function getAvailableFactColumns() + { + return [ + 'services_cnt' => 'services_total', + 'services_critical' => 'services_critical_handled + f.services_critical_unhandled', + 'services_unhandled_critical' => 'services_critical_unhandled', + 'services_warning' => 'services_warning_handled + f.services_warning_unhandled', + 'services_unhandled_warning' => 'services_warning_unhandled', + 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled', + 'services_unhandled_unknown' => 'services_unknown_unhandled', + ]; + } + + public function listAvailableDimensions() + { + $db = $this->getDb(); + + $query = CustomvarFlat::on($db); + $this->applyRestrictions($query); + + $query + ->columns('flatname') + ->orderBy('flatname') + ->filter(Filter::like('service.id', '*')); + $query->getSelectBase()->groupBy('flatname'); + + $dimensions = []; + foreach ($query as $row) { + // Replaces array index notations with [*] to get results for arbitrary indexes + $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->flatname); + $dimensions[$name] = $name; + } + + return $dimensions; + } + + public function prepareInnerQuery() + { + $query = ServicestateSummary::on($this->getDb()); + $query->columns(array_diff_key($query->getModel()->getColumns(), (new Service())->getColumns())); + $query->disableDefaultSort(); + $this->applyRestrictions($query); + + return $query; + } + + /** + * Return Filter for Services cube. + * + * @return Filter\Any|Filter\Chain + */ + public function getObjectsFilter() + { + if ($this->objectsFilter === null) { + $this->finalizeInnerQuery(); + + $services = $this->innerQuery()->columns([ + 'host_name' => 'host.name', + 'service_name' => 'service.name' + ]); + + $services->getSelectBase()->resetGroupBy(); + $filter = Filter::any(); + + foreach ($services as $service) { + $filter->add( + Filter::all( + Filter::equal('service.name', $service->service_name), + Filter::equal('host.name', $service->host_name) + ) + ); + } + + $this->objectsFilter = $filter; + } + + return $this->objectsFilter; + } +} diff --git a/library/Cube/Ido/CustomVarDimension.php b/library/Cube/Ido/CustomVarDimension.php new file mode 100644 index 0000000..0c21282 --- /dev/null +++ b/library/Cube/Ido/CustomVarDimension.php @@ -0,0 +1,158 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido; + +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Dimension; + +/** + * CustomVarDimension + * + * This provides dimenstions for custom variables available in the IDO + * + * TODO: create safe aliases for special characters + * + * @package Icinga\Module\Cube\Ido + */ +class CustomVarDimension implements Dimension +{ + public const TYPE_HOST = 'host'; + + public const TYPE_SERVICE = 'service'; + + /** + * @var string custom variable name + */ + protected $varName; + + /** + * @var string custom variable label + */ + protected $varLabel; + + /** + * @var bool Whether null values should be shown + */ + protected $wantNull = false; + + /** @var string Type of the custom var */ + protected $type; + + /** + * CustomVarDimension constructor. + * + * @param $varName + * @param string $type Type of the custom var + */ + public function __construct($varName, $type = null) + { + $this->varName = $varName; + $this->type = $type; + } + + /** + * Define whether null values should be shown + * + * @param bool $wantNull + * @return $this + */ + public function wantNull($wantNull = true) + { + $this->wantNull = $wantNull; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->varName; + } + + /** + * @return string + */ + public function getLabel() + { + return $this->varLabel ?: $this->getName(); + } + + /** + * Set the label + * + * @param string $label + * @return $this + */ + public function setLabel($label) + { + $this->varLabel = $label; + + return $this; + } + + /** + * Add a label + * + * @param string $label + * @return $this + */ + public function addLabel($label) + { + if ($this->varLabel === null) { + $this->setLabel($label); + } else { + $this->varLabel .= ' & ' . $label; + } + + return $this; + } + + public function getColumnExpression(Cube $cube) + { + /** @var IdoCube $cube */ + if ($this->wantNull) { + return 'COALESCE(' . $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']) . ", '-')"; + } else { + return $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']); + } + } + + protected function safeVarname($name) + { + return $name; + } + + public function addToCube(Cube $cube) + { + switch ($this->type) { + case self::TYPE_HOST: + $objectId = 'ho.object_id'; + break; + case self::TYPE_SERVICE: + $objectId = 'so.object_id'; + break; + default: + $objectId = 'o.object_id'; + } + $name = $this->safeVarname($this->varName); + /** @var $cube IdoCube */ + $alias = $cube->db()->quoteIdentifier(['c_' . $name]); + + if ($cube->isPgsql()) { + $on = "LOWER($alias.varname) = ?"; + $name = strtolower($name); + } else { + $on = $alias . '.varname = ? COLLATE latin1_general_ci'; + } + + $cube->innerQuery()->joinLeft( + array($alias => $cube->tableName('icinga_customvariablestatus')), + $cube->db()->quoteInto($on, $name) + . ' AND ' . $alias . '.object_id = ' . $objectId, + array() + )->group($alias . '.varvalue'); + } +} diff --git a/library/Cube/Ido/DataView/Hoststatus.php b/library/Cube/Ido/DataView/Hoststatus.php new file mode 100644 index 0000000..32ee44b --- /dev/null +++ b/library/Cube/Ido/DataView/Hoststatus.php @@ -0,0 +1,17 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido\DataView; + +use Icinga\Data\ConnectionInterface; +use Icinga\Module\Cube\Ido\Query\HoststatusQuery; + +class Hoststatus extends \Icinga\Module\Monitoring\DataView\Hoststatus +{ + public function __construct(ConnectionInterface $connection, array $columns = null) + { + $this->connection = $connection; + $this->query = new HoststatusQuery($connection->getResource(), $columns); + } +} diff --git a/library/Cube/Ido/DbCube.php b/library/Cube/Ido/DbCube.php new file mode 100644 index 0000000..5fc5b47 --- /dev/null +++ b/library/Cube/Ido/DbCube.php @@ -0,0 +1,298 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido; + +use Icinga\Data\Db\DbConnection; +use Icinga\Module\Cube\Cube; + +abstract class DbCube extends Cube +{ + /** @var DbConnection */ + protected $connection; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** @var ZfSelectWrapper The inner query fetching all required data */ + protected $innerQuery; + + /** @var \Zend_Db_Select The rollup query, creating grouped sums over innerQuery */ + protected $rollupQuery; + + /** @var \Zend_Db_Select The outer query, orders respecting NULL values, rollup first */ + protected $fullQuery; + + /** @var string Database name. Allows to eventually join over multiple dbs */ + protected $dbName; + + /** @var array Key/value array containing our chosen facts and the corresponding SQL expression */ + protected $factColumns = array(); + + /** + * A DbCube must provide a list of all available columns + * + * This is a key/value array with the key being the fact name / column alias + * and + * + * @return array + */ + abstract public function getAvailableFactColumns(); + + /** + * @return \Zend_Db_Select + */ + abstract public function prepareInnerQuery(); + + /** + * Set a database connection + * + * @param DbConnection $connection + * @return $this + */ + public function setConnection(DbConnection $connection) + { + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + return $this; + } + + /** + * Prepare the query and fetch all data + * + * @return array + */ + public function fetchAll() + { + $query = $this->fullQuery(); + return $this->db()->fetchAll($query); + } + + /** + * Choose a one or more facts + * + * This also initializes a fact column lookup array + * + * @param array $facts + * @return $this + */ + public function chooseFacts(array $facts) + { + parent::chooseFacts($facts); + + $this->factColumns = array(); + $columns = $this->getAvailableFactColumns(); + foreach ($this->chosenFacts as $name) { + $this->factColumns[$name] = $columns[$name]; + } + + return $this; + } + + /** + * @param $name + * @return $this + */ + public function setDbName($name) + { + $this->dbName = $name; + return $this; + } + + /** + * Gives back the table name, eventually prefixed with a defined DB name + * + * @param string $name + * @return string + */ + public function tableName($name) + { + if ($this->dbName === null) { + return $name; + } else { + return $this->dbName . '.' . $name; + } + } + + /** + * Returns an eventually defined DB name + * + * @return string|null + */ + public function getDbName() + { + return $this->dbName; + } + + /** + * Get our inner query + * + * Hint: mostly used to get rid of NULL values + * + * @return ZfSelectWrapper + */ + public function innerQuery() + { + if ($this->innerQuery === null) { + $this->innerQuery = new ZfSelectWrapper($this->prepareInnerQuery()); + } + + return $this->innerQuery; + } + + /** + * We first prepare the queries and to finalize it later on + * + * This way dimensions can be added one by one, they will be allowed to + * optionally join additional tables or apply other modifications late + * in the process + * + * @return void + */ + public function finalizeInnerQuery() + { + $query = $this->innerQuery()->unwrap(); + $columns = array(); + foreach ($this->dimensions as $name => $dimension) { + $dimension->addToCube($this); + if ($this->hasSlice($name)) { + $query->where( + $dimension->getColumnExpression($this) . ' = ?', + $this->slices[$name] + ); + } else { + $columns[$name] = $dimension->getColumnExpression($this); + } + } + + $c = []; + + foreach ($columns + $this->factColumns as $k => $v) { + $c[$this->db()->quoteIdentifier([$k])] = $v; + } + + $query->columns($c); + } + + /** + * Lazy-load our full query + * + * @return \Zend_Db_Select + */ + protected function fullQuery() + { + if ($this->fullQuery === null) { + $this->fullQuery = $this->prepareFullQuery(); + } + + return $this->fullQuery; + } + + /** + * Lazy-load our full query + * + * @return \Zend_Db_Select + */ + protected function rollupQuery() + { + if ($this->rollupQuery === null) { + $this->rollupQuery = $this->prepareRollupQuery(); + } + + return $this->rollupQuery; + } + + /** + * The full query wraps the rollup query in a sub-query to work around + * MySQL limitations. This is required to not get into trouble when ordering, + * especially combined with the need to keep control over (eventually desired) + * NULL value fact columns + * + * @return \Zend_Db_Select + */ + protected function prepareFullQuery() + { + $alias = 'rollup'; + $cols = $this->listColumns(); + + $columns = array(); + + foreach ($cols as $col) { + $columns[$this->db()->quoteIdentifier([$col])] = $alias . '.' . $this->db()->quoteIdentifier([$col]); + } + + $select = $this->db()->select()->from( + array($alias => $this->rollupQuery()), + $columns + ); + + foreach ($columns as $col) { + $select->order('(' . $col . ' IS NOT NULL)'); + $select->order($col); + } + + return $select; + } + + /** + * Provide access to our DB + * + * @return \Zend_Db_Adapter_Abstract + */ + public function db() + { + return $this->db; + } + + /** + * Whether our connection is PostgreSQL + * + * @return bool + */ + public function isPgsql() + { + return $this->connection->getDbType() === 'pgsql'; + } + + + /** + * This prepares the rollup query + * + * Inner query is wrapped in a subquery, summaries for all facts are + * fetched. Rollup considers all defined dimensions and expects them + * to exist as columns in the innerQuery + * + * @return \Zend_Db_Select + */ + protected function prepareRollupQuery() + { + $alias = 'sub'; + + $dimensions = array_map(function ($val) { + return $this->db()->quoteIdentifier([$val]); + }, array_keys($this->listDimensions())); + $this->finalizeInnerQuery(); + $columns = array(); + foreach ($dimensions as $dimension) { + $columns[$dimension] = $alias . '.' . $dimension; + } + + foreach ($this->listFacts() as $fact) { + $columns[$fact] = 'SUM(' . $fact . ')'; + } + + $select = $this->db()->select()->from( + array($alias => $this->innerQuery()->unwrap()), + $columns + ); + + if ($this->isPgsql()) { + $select->group('ROLLUP (' . implode(', ', $dimensions) . ')'); + } else { + $select->group('(' . implode('), (', $dimensions) . ') WITH ROLLUP'); + } + + return $select; + } +} diff --git a/library/Cube/Ido/IdoCube.php b/library/Cube/Ido/IdoCube.php new file mode 100644 index 0000000..ca76b21 --- /dev/null +++ b/library/Cube/Ido/IdoCube.php @@ -0,0 +1,198 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido; + +use Icinga\Application\Config; +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\QueryException; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Util\GlobFilter; + +/** + * IdoCube + * + * Base class for IDO-related cubes + * + * @package Icinga\Module\Cube\Ido + */ +abstract class IdoCube extends DbCube +{ + /** @var array */ + protected $availableFacts = array(); + + /** @var string We ask for the IDO version for compatibility reasons */ + protected $idoVersion; + + /** @var MonitoringBackend */ + protected $backend; + + /** + * Cache for {@link filterProtectedCustomvars()} + * + * @var string|null + */ + protected $protectedCustomvars; + + /** @var GlobFilter The properties to hide from the user */ + protected $blacklistedProperties; + + /** + * We can steal the DB connection directly from a Monitoring backend + * + * @param MonitoringBackend $backend + * @return $this + */ + public function setBackend(MonitoringBackend $backend) + { + $this->backend = $backend; + + $resource = $backend->getResource(); + $resource->getDbAdapter() + ->getConnection() + ->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_NATURAL); + + $this->setConnection($resource); + + return $this; + } + + /** + * Provice access to our DB resource + * + * This lazy-loads the default monitoring backend in case no DB has been + * given + * + * @return \Zend_Db_Adapter_Abstract + */ + public function db() + { + $this->requireBackend(); + return parent::db(); + } + + /** + * Returns the Icinga IDO version + * + * @return string + */ + protected function getIdoVersion() + { + if ($this->idoVersion === null) { + $db = $this->db(); + $this->idoVersion = $db->fetchOne( + $db->select()->from('icinga_dbversion', 'version') + ); + } + + return $this->idoVersion; + } + + /** + * Steal the default monitoring DB resource... + * + * ...in case none has been defined otherwise + * + * @return void + */ + protected function requireBackend() + { + if ($this->db === null) { + $this->setBackend(MonitoringBackend::instance()); + } + } + + protected function getMonitoringRestriction() + { + $restriction = Filter::matchAny(); + $restriction->setAllowedFilterColumns(array( + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + function ($c) { + return preg_match('/^_(?:host|service)_/i', $c); + } + )); + + $filters = Auth::getInstance()->getUser()->getRestrictions('monitoring/filter/objects'); + + foreach ($filters as $filter) { + if ($filter === '*') { + return Filter::matchAny(); + } + try { + $restriction->addFilter(Filter::fromQueryString($filter)); + } catch (QueryException $e) { + throw new ConfigurationError( + 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s', + 'monitoring/filter/objects', + $filter, + implode(', ', array( + 'instance_name', + 'host_name', + 'hostgroup_name', + 'service_description', + 'servicegroup_name', + '_(host|service)_<customvar-name>' + )), + $e + ); + } + } + + return $restriction; + } + + /** + * Return the given array without values matching the custom variables protected by the monitoring module + * + * @param string[] $customvars + * + * @return string[] + */ + protected function filterProtectedCustomvars(array $customvars) + { + if ($this->blacklistedProperties === null) { + $this->blacklistedProperties = new GlobFilter( + Auth::getInstance()->getRestrictions('monitoring/blacklist/properties') + ); + } + + if ($this instanceof IdoServiceStatusCube) { + $type = 'service'; + } else { + $type = 'host'; + } + + $customvars = $this->blacklistedProperties->removeMatching( + [$type => ['vars' => array_flip($customvars)]] + ); + + $customvars = isset($customvars[$type]['vars']) ? array_flip($customvars[$type]['vars']) : []; + + if ($this->protectedCustomvars === null) { + $config = Config::module('monitoring')->get('security', 'protected_customvars'); + $protectedCustomvars = array(); + + foreach (preg_split('~,~', $config, -1, PREG_SPLIT_NO_EMPTY) as $pattern) { + $regex = array(); + foreach (explode('*', $pattern) as $literal) { + $regex[] = preg_quote($literal, '/'); + } + + $protectedCustomvars[] = implode('.*', $regex); + } + + $this->protectedCustomvars = empty($protectedCustomvars) + ? '/^$/' + : '/^(?:' . implode('|', $protectedCustomvars) . ')$/'; + } + + return preg_grep($this->protectedCustomvars, $customvars, PREG_GREP_INVERT); + } +} diff --git a/library/Cube/Ido/IdoHostStatusCube.php b/library/Cube/Ido/IdoHostStatusCube.php new file mode 100644 index 0000000..4881a9b --- /dev/null +++ b/library/Cube/Ido/IdoHostStatusCube.php @@ -0,0 +1,112 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido; + +use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer; +use Icinga\Module\Cube\Ido\DataView\Hoststatus; + +class IdoHostStatusCube extends IdoCube +{ + public function getRenderer() + { + return new HostStatusCubeRenderer($this); + } + + /** + * @inheritdoc + */ + public function getAvailableFactColumns() + { + return array( + 'hosts_cnt' => 'SUM(CASE WHEN hs.has_been_checked = 1 THEN 1 ELSE 0 END)', + 'hosts_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1' + . ' THEN 1 ELSE 0 END)', + 'hosts_unhandled_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1' + . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0' + . ' THEN 1 ELSE 0 END)', + 'hosts_unreachable' => 'SUM(CASE WHEN hs.current_state = 2 THEN 1 ELSE 0 END)', + 'hosts_unhandled_unreachable' => 'SUM(CASE WHEN hs.current_state = 2' + . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0' + . ' THEN 1 ELSE 0 END)', + ); + } + + public function createDimension($name) + { + $this->registerAvailableDimensions(); + + if (isset($this->availableDimensions[$name])) { + return clone $this->availableDimensions[$name]; + } + + return new CustomVarDimension($name, CustomVarDimension::TYPE_HOST); + } + + /** + * Add a specific named dimension + * + * Right now this are just custom vars, we might support group memberships + * or other properties in future + * + * @param string $name + * @return $this + */ + public function addDimensionByName($name) + { + if (count($this->filterProtectedCustomvars(array($name))) === 1) { + $this->addDimension($this->createDimension($name)); + } + + return $this; + } + + /** + * This returns a list of all available Dimensions + * + * @return array + */ + public function listAvailableDimensions() + { + $this->requireBackend(); + + $view = $this->backend->select()->from('hoststatus'); + + $view->applyFilter($this->getMonitoringRestriction()); + + $select = $view->getQuery()->clearOrder()->getSelectQuery(); + + $select + ->columns('cv.varname') + ->join( + ['cv' => $this->tableName('icinga_customvariablestatus')], + 'cv.object_id = ho.object_id', + [] + ) + ->group('cv.varname'); + + if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) { + $select->where('cv.is_json = 0'); + } + + $select->order('cv.varname'); + + return $this->filterProtectedCustomvars($this->db()->fetchCol($select)); + } + + public function prepareInnerQuery() + { + $this->requireBackend(); + + $view = new Hoststatus($this->backend); + + $view->getQuery()->requireColumn('host_state'); + + $view->applyFilter($this->getMonitoringRestriction()); + + $select = $view->getQuery()->clearOrder()->getSelectQuery(); + + return $select; + } +} diff --git a/library/Cube/Ido/IdoServiceStatusCube.php b/library/Cube/Ido/IdoServiceStatusCube.php new file mode 100644 index 0000000..10a172a --- /dev/null +++ b/library/Cube/Ido/IdoServiceStatusCube.php @@ -0,0 +1,113 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido; + +use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer; + +class IdoServiceStatusCube extends IdoCube +{ + public function getRenderer() + { + return new ServiceStatusCubeRenderer($this); + } + + public function getAvailableFactColumns() + { + return [ + 'services_cnt' => 'SUM(CASE WHEN ss.has_been_checked = 1 THEN 1 ELSE 0 END)', + 'services_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2' + . ' THEN 1 ELSE 0 END)', + 'services_unhandled_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2' + . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0' + . ' THEN 1 ELSE 0 END)', + 'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)', + 'services_unhandled_warning' => 'SUM(CASE WHEN ss.current_state = 1' + . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0' + . ' THEN 1 ELSE 0 END)', + 'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)', + 'services_unhandled_unknown' => 'SUM(CASE WHEN ss.current_state = 3' + . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0' + . ' THEN 1 ELSE 0 END)', + ]; + } + + /** + * This returns a list of all available Dimensions + * + * @return array + */ + public function listAvailableDimensions() + { + $this->requireBackend(); + + $view = $this->backend->select()->from('servicestatus'); + + $view->applyFilter($this->getMonitoringRestriction()); + + $select = $view->getQuery()->clearOrder()->getSelectQuery(); + + $select + ->columns('cv.varname') + ->join( + ['cv' => $this->tableName('icinga_customvariablestatus')], + 'cv.object_id = so.object_id', + [] + ) + ->group('cv.varname'); + + if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) { + $select->where('cv.is_json = 0'); + } + + $select->order('cv.varname'); + + return $this->filterProtectedCustomvars($this->db()->fetchCol($select)); + } + + public function prepareInnerQuery() + { + $this->requireBackend(); + + $view = $this->backend->select()->from('servicestatus'); + + $view->getQuery()->requireColumn('service_state'); + + $view->applyFilter($this->getMonitoringRestriction()); + + $select = $view->getQuery()->clearOrder()->getSelectQuery(); + + return $select; + } + + /** + * Add a specific named dimension + * + * Right now this are just custom vars, we might support group memberships + * or other properties in future + * + * @param string $name + * + * @return $this + */ + public function addDimensionByName($name) + { + if (count($this->filterProtectedCustomvars([$name])) === 1) { + $this->addDimension($this->createDimension($name)); + } + + return $this; + } + + public function createDimension($name) + { + $this->registerAvailableDimensions(); + + if (isset($this->availableDimensions[$name])) { + return clone $this->availableDimensions[$name]; + } + + return new CustomVarDimension($name, CustomVarDimension::TYPE_SERVICE); + } +} diff --git a/library/Cube/Ido/Query/HoststatusQuery.php b/library/Cube/Ido/Query/HoststatusQuery.php new file mode 100644 index 0000000..6a9aa96 --- /dev/null +++ b/library/Cube/Ido/Query/HoststatusQuery.php @@ -0,0 +1,47 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Ido\Query; + +use Exception; +use Icinga\Application\Version; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Exception\NotImplementedError; +use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery; + +class HoststatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery +{ + protected $subQueryTargets = array( + 'hostgroups' => 'hostgroup', + 'servicegroups' => 'servicegroup', + 'services' => 'servicestatus' + ); + + protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter) + { + if ($name === 'servicestatus') { + return ['s.host_object_id', 'ho.object_id']; + } + + return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter); + } + + protected function createSubQueryFilter(FilterExpression $filter, $queryName) + { + try { + return parent::createSubQueryFilter($filter, $queryName); + } catch (Exception $e) { + if (version_compare(Version::VERSION, '2.10.0', '>=')) { + throw $e; + } + + if ($e->getMessage() === 'Undefined array key 0' && basename($e->getFile()) === 'IdoQuery.php') { + // Ensures compatibility with earlier Icinga Web 2 versions + throw new NotImplementedError(''); + } else { + throw $e; + } + } + } +} diff --git a/library/Cube/Ido/ZfSelectWrapper.php b/library/Cube/Ido/ZfSelectWrapper.php new file mode 100644 index 0000000..745d7a5 --- /dev/null +++ b/library/Cube/Ido/ZfSelectWrapper.php @@ -0,0 +1,77 @@ +<?php + +namespace Icinga\Module\Cube\Ido; + +/** + * Since version 1.1.0 we're using the monitoring module's queries as the cubes' base queries. + * Before, the host object table was available using the alias 'o'. Now it's 'ho'. + * Without this wrapper, the action link hook provided by the director would fail because it relies on the alias 'o'. + */ +class ZfSelectWrapper +{ + /** @var \Zend_Db_Select */ + protected $select; + + public function __construct(\Zend_Db_Select $select) + { + $this->select = $select; + } + + /** + * Get the underlying Zend_Db_Select query + * + * @return \Zend_Db_Select + */ + public function unwrap() + { + return $this->select; + } + + /** + * {@see \Zend_Db_Select::reset()} + */ + public function reset($part = null) + { + $this->select->reset($part); + + return $this; + } + + /** + * {@see \Zend_Db_Select::columns()} + */ + public function columns($cols = '*', $correlationName = null) + { + if (is_array($cols)) { + foreach ($cols as $alias => &$col) { + if (substr($col, 0, 2) === 'o.') { + $col = 'ho.' . substr($col, 2); + } + } + } + + return $this->select->columns($cols, $correlationName); + } + + /** + * Proxy Zend_Db_Select method calls + * + * @param string $name The name of the method to call + * @param array $arguments Arguments for the method to call + * + * @return mixed + * + * @throws \BadMethodCallException If the called method does not exist + */ + public function __call($name, array $arguments) + { + if (! method_exists($this->select, $name)) { + $class = get_class($this); + $message = "Call to undefined method $class::$name"; + + throw new \BadMethodCallException($message); + } + + return call_user_func_array([$this->select, $name], $arguments); + } +} diff --git a/library/Cube/ProvidedHook/Cube/IcingaDbActions.php b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php new file mode 100644 index 0000000..c670ab4 --- /dev/null +++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php @@ -0,0 +1,41 @@ +<?php + +namespace Icinga\Module\Cube\ProvidedHook\Cube; + +use Icinga\Module\Cube\Hook\IcingaDbActionsHook; +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Module\Cube\Icingadb\IcingadbServiceStatusCube; + +class IcingaDbActions extends IcingaDbActionsHook +{ + public function createActionLinks(IcingaDbCube $cube) + { + $type = 'host'; + if ($cube instanceof IcingadbServiceStatusCube) { + $type = 'service'; + } + + $url = 'icingadb/' . $type . 's'; + + $paramsWithPrefix = []; + foreach ($cube->getSlices() as $dimension => $slice) { + $paramsWithPrefix[$type . '.vars.' . $dimension] = $slice; + } + + if ($type === 'host') { + $this->addActionLink( + $this->makeUrl($url, $paramsWithPrefix), + t('Show hosts status'), + t('This shows all matching hosts and their current state in Icinga DB Web'), + 'server' + ); + } else { + $this->addActionLink( + $this->makeUrl($url, $paramsWithPrefix), + t('Show services status'), + t('This shows all matching hosts and their current state in Icinga DB Web'), + 'cog' + ); + } + } +} diff --git a/library/Cube/ProvidedHook/Cube/MonitoringActions.php b/library/Cube/ProvidedHook/Cube/MonitoringActions.php new file mode 100644 index 0000000..ae65c67 --- /dev/null +++ b/library/Cube/ProvidedHook/Cube/MonitoringActions.php @@ -0,0 +1,53 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\ProvidedHook\Cube; + +use Icinga\Module\Cube\Hook\ActionsHook; +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Ido\IdoHostStatusCube; +use Icinga\Module\Cube\Ido\IdoServiceStatusCube; +use Icinga\Web\View; + +/** + * MonitoringActionLinks + * + * An action link hook implementation linking to matching hosts/services in the + * monitoring module + */ +class MonitoringActions extends ActionsHook +{ + public function prepareActionLinks(Cube $cube, View $view) + { + if ($cube instanceof IdoHostStatusCube) { + $vars = []; + foreach ($cube->getSlices() as $key => $val) { + $vars['_host_' . $key] = $val; + } + + $url = 'monitoring/list/hosts'; + + $this->addActionLink( + $this->makeUrl($url, $vars), + $view->translate('Show hosts status'), + $view->translate('This shows all matching hosts and their current state in the monitoring module'), + 'host' + ); + } elseif ($cube instanceof IdoServiceStatusCube) { + $vars = []; + foreach ($cube->getSlices() as $key => $val) { + $vars['_service_' . $key] = $val; + } + + $url = 'monitoring/list/services'; + + $this->addActionLink( + $this->makeUrl($url, $vars), + $view->translate('Show services status'), + $view->translate('This shows all matching services and their current state in the monitoring module'), + 'host' + ); + } + } +} diff --git a/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php new file mode 100644 index 0000000..a32310a --- /dev/null +++ b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php @@ -0,0 +1,11 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\ProvidedHook\Icingadb; + +use Icinga\Module\Icingadb\Hook\IcingadbSupportHook; + +class IcingadbSupport extends IcingadbSupportHook +{ +} diff --git a/library/Cube/Web/ActionLink.php b/library/Cube/Web/ActionLink.php new file mode 100644 index 0000000..c9ad87b --- /dev/null +++ b/library/Cube/Web/ActionLink.php @@ -0,0 +1,103 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Web; + +use Icinga\Web\Url; +use Icinga\Web\View; + +/** + * ActionLink + * + * ActionLinksHook implementations return instances of this class + * + * @package Icinga\Module\Cube\Web + */ +class ActionLink +{ + /** @var Url */ + protected $url; + + /** @var string */ + protected $title; + + /** @var string */ + protected $description; + + /** @var string */ + protected $icon; + + /** + * ActionLink constructor. + * @param Url $url + * @param string $title + * @param string $description + * @param string $icon + */ + public function __construct(Url $url, $title, $description, $icon) + { + $this->url = $url; + $this->title = $title; + $this->description = $description; + $this->icon = $icon; + } + + /** + * @return Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return string + */ + public function getIcon() + { + return $this->icon; + } + + /** + * Render our icon + * + * @param View $view + * @return string + */ + protected function renderIcon(View $view) + { + return $view->icon($this->getIcon()); + } + + /** + * @param View $view + * @return string + */ + public function render(View $view) + { + return sprintf( + '<a href="%s">%s<span class="title">%s</span><p>%s</p></a>', + $this->getUrl(), + $this->renderIcon($view), + $view->escape($this->getTitle()), + $view->escape($this->getDescription()) + ); + } +} diff --git a/library/Cube/Web/ActionLinks.php b/library/Cube/Web/ActionLinks.php new file mode 100644 index 0000000..0c6cba4 --- /dev/null +++ b/library/Cube/Web/ActionLinks.php @@ -0,0 +1,115 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Web; + +use Exception; +use Icinga\Application\Hook; +use Icinga\Module\Cube\Cube; +use Icinga\Module\Cube\Hook\ActionsHook; +use Icinga\Web\View; + +/** + * ActionLink + * + * ActionsHook implementations return instances of this class + * + * @package Icinga\Module\Cube\Web + */ +class ActionLinks +{ + /** @var ActionLink[] */ + protected $links = array(); + + /** + * Get all links for all Hook implementations + * + * This is what the Cube calls when rendering details + * + * @param Cube $cube + * @param View $view + * + * @return string + */ + public static function renderAll(Cube $cube, View $view) + { + $html = array(); + + /** @var ActionsHook $hook */ + foreach (Hook::all('Cube/Actions') as $hook) { + try { + $hook->prepareActionLinks($cube, $view); + } catch (Exception $e) { + $html[] = static::renderErrorItem($e, $view); + } + + foreach ($hook->getActionLinks()->getLinks() as $link) { + $html[] = '<li>' . $link->render($view) . '</li>'; + } + } + + if (empty($html)) { + $html[] = static::renderErrorItem( + $view->translate('No action links have been provided for this cube'), + $view + ); + } + + return implode("\n", $html) . "\n"; + } + + /** + * @param Exception|string $error + * @param View $view + * @return string + */ + private static function renderErrorItem($error, View $view) + { + if ($error instanceof Exception) { + $error = $error->getMessage(); + } + return '<li class="error">' . $view->escape($error) . '</li>'; + } + + /** + * Add an ActionLink to this set of actions + * + * @param ActionLink $link + * @return $this + */ + public function add(ActionLink $link) + { + $this->links[] = $link; + return $this; + } + + /** + * @return ActionLink[] + */ + public function getLinks() + { + return $this->links; + } + + /** + * @param View $view + * + * @return string + */ + public function render(View $view) + { + $links = $this->getLinks(); + if (empty($links)) { + return ''; + } + + $html = '<ul class="action-links">'; + foreach ($links as $link) { + $html .= $link->render($view); + } + $html .= '</ul>'; + + return $html; + } +} diff --git a/library/Cube/Web/Controller.php b/library/Cube/Web/Controller.php new file mode 100644 index 0000000..b7405f9 --- /dev/null +++ b/library/Cube/Web/Controller.php @@ -0,0 +1,115 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Web; + +use Icinga\Application\Modules\Module; +use Icinga\Module\Cube\DimensionParams; +use Icinga\Module\Cube\Forms\DimensionsForm; +use Icinga\Module\Cube\Hook\IcingaDbActionsHook; +use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Web\Controller as WebController; +use Icinga\Web\View; + +abstract class Controller extends WebController +{ + /** @var View This helps IDEs to understand that this is not ZF view */ + public $view; + + /** @var \Icinga\Module\Cube\Cube */ + protected $cube; + + /** + * Return this controllers' cube + * + * @return \Icinga\Module\Cube\Cube + */ + abstract protected function getCube(); + + protected function moduleInit() + { + $this->cube = $this->getCube(); + } + + public function detailsAction() + { + $this->getTabs()->add('details', [ + 'label' => $this->translate('Cube details'), + 'url' => $this->getRequest()->getUrl() + ])->activate('details'); + + $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns())); + $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions(); + $wantNull = $this->params->shift('wantNull'); + + foreach ($vars as $var) { + $this->cube->addDimensionByName($var); + if ($wantNull) { + $this->cube->getDimension($var)->wantNull(); + } + } + + foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) { + $this->params->shift($p); + } + + foreach ($this->params->toArray() as $param) { + $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1])); + } + + $this->view->title = $this->cube->getSlicesLabel(); + if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) { + $this->view->links = IcingaDbActionsHook::renderAll($this->cube); + } else { + $this->view->links = ActionLinks::renderAll($this->cube, $this->view); + } + + $this->render('cube-details', null, true); + } + + protected function renderCube() + { + // Hint: order matters, we are shifting! + $showSettings = $this->params->shift('showSettings'); + + $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns())); + $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions(); + $wantNull = $this->params->shift('wantNull'); + + foreach ($vars as $var) { + $this->cube->addDimensionByName($var); + if ($wantNull) { + $this->cube->getDimension($var)->wantNull(); + } + } + + foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) { + $this->params->shift($p); + } + + foreach ($this->params->toArray() as $param) { + $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1])); + } + + $this->view->title = sprintf( + $this->translate('Cube: %s'), + $this->cube->getPathLabel() + ); + + if (count($this->cube->listDimensions()) > 0) { + $this->view->cube = $this->cube; + } else { + $showSettings = true; + } + + if ($showSettings) { + $this->view->form = (new DimensionsForm())->setCube($this->cube); + $this->view->form->handleRequest(); + } else { + $this->setAutorefreshInterval(15); + } + + $this->render('cube-index', null, true); + } +} diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php new file mode 100644 index 0000000..2a024ae --- /dev/null +++ b/library/Cube/Web/IdoController.php @@ -0,0 +1,24 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Web; + +use Icinga\Web\Widget\Tabextension\DashboardAction; + +abstract class IdoController extends Controller +{ + public function createTabs() + { + return $this->getTabs() + ->add('cube/hosts', [ + 'label' => $this->translate('Hosts'), + 'url' => 'cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString()) + ]) + ->add('cube/services', [ + 'label' => $this->translate('Services'), + 'url' => 'cube/services' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString()) + ]) + ->extend(new DashboardAction()); + } +} diff --git a/module.info b/module.info new file mode 100644 index 0000000..374e5b2 --- /dev/null +++ b/module.info @@ -0,0 +1,10 @@ +Name: Cube +Version: 1.2.2 +Requires: + Libraries: icinga-php-library (>=0.9.0) + Modules: monitoring (>= 2.9.0), icingadb (>= 1.0.0) +Description: Cube for Icinga Web + The Cube allows you to analyze data across multiple dimensions in your + Icinga Web frontend. Currently it shows host statistics (total count, + health) grouped by various custom variables in multiple dimensions. It + integrates well with other modules and provides extensible hooks diff --git a/public/css/module.less b/public/css/module.less new file mode 100644 index 0000000..150bb5a --- /dev/null +++ b/public/css/module.less @@ -0,0 +1,343 @@ +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +.controls > a { + color: @icinga-blue; + + &:hover::before { + text-decoration: none; + } +} + +div.cube { + div.cube-dimension0 > .header { + font-size: 1.2em; + font-weight: bold; + border-bottom: 1px solid @text-color; + } + + div.cube-dimension1 { + clear: both; + border-left: 0.5em solid transparent; + } + + div.cube-dimension1 > .header, + div.cube-dimension2 { + display: inline-block; + width: 10em; + height: 10em; + margin: 0; + padding: 0.2em; + margin-top: 0.3em; + overflow: hidden; + } + + div.cube-dimension1 > .header { + display: flex; + float: left; + font-weight: bold; + text-align: center; + padding-left: 0; + + > a:first-child { + flex: 1; + max-width: 8em; + word-wrap: break-word; + } + } + + div.cube-dimension2 { + > .header { + display: flex; + text-align: center; + color: @text-color-on-icinga-blue; + margin-bottom: 1em; + + > a:first-child { + flex: 1; + max-width: 8em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + background: @color-ok; + + &.critical { + background: @color-critical; + } + + &.warning { + background: @color-warning; + } + + &.unknown { + background: @color-unknown; + } + + &.unreachable { + background: @color-unreachable; + } + + &.ok { + background: @color-ok; + } + + > .body { + text-align: center; + } + } + + span.critical, span.warning, span.unknown, span.unreachable, span.ok { + display: inline; + border-radius: 0.1em; + padding: 0.05em 0.2em; + color: @text-color-on-icinga-blue; + font-size: 3em; + } + + span.ok { + background: @color-ok; + } + + .others { + display: flex; + justify-content: center; + span { + margin-left: 0.2em; + margin-top: 0.8em; + font-size: 1em; + } + } + + span.critical { + background: @color-critical; + } + + span.critical.handled { + background: @color-critical-handled; + } + + div.cube-dimension0.critical > .header { + color: @color-critical; + border-color: @color-critical; + } + + div.cube-dimension1.critical { + border-left-color: @color-critical; + + &.handled { + border-left-color: @color-critical-handled; + } + } + + div.cube-dimension2.critical { + span.sum { + font-size: 1em; + } + } + + span.warning { + background: @color-warning; + } + + span.warning.handled { + background: @color-warning-handled; + } + + div.cube-dimension0.warning > .header { + color: @color-warning; + border-color: @color-warning; + } + + div.cube-dimension1.warning { + border-left-color: @color-warning; + + &.handled { + border-left-color: @color-warning-handled; + } + } + + div.cube-dimension2.warning { + span.sum { + font-size: 1em; + } + } + + span.unknown{ + background: @color-unknown; + } + + span.unknown.handled { + background: @color-unknown-handled; + } + + div.cube-dimension0.unknown > .header { + color: @color-unknown; + border-color: @color-unknown; + } + + div.cube-dimension1.unknown { + border-left-color: @color-unknown; + + &.handled { + border-left-color: @color-unknown-handled; + } + } + + div.cube-dimension2.unknown { + span.sum { + font-size: 1em; + } + } +} + +span.unreachable{ + background: @color-unreachable; +} + +span.unreachable.handled { + background: @color-unreachable-handled; +} + +div.cube-dimension0.unreachable > .header { + color: @color-unreachable; + border-color: @color-unreachable; +} + +div.cube-dimension1.unreachable { + border-left-color: @color-unreachable; + + &.handled { + border-left-color: @color-unreachable-handled; + } +} + +div.cube-dimension2.unreachable { + span.sum { + font-size: 1em; + } +} + +fieldset.dimensions { + margin: 0; + padding: 0; + + span { + min-width: 18em; + display: inline-block; + } + + button[type=submit] { + border: none; + color: @text-color; + background: none; + + padding-left: .75em; + padding-right: .75em; + + i:before { + margin-right: 0; + } + + &:not([disabled]):hover { + .rounded-corners(); + background-color: @icinga-blue; + color: @text-color-on-icinga-blue; + } + } + + button[type=submit][disabled] { + color: @disabled-gray; + background-color: @body-bg-color; + cursor: auto; + } + + &:first-of-type button[type=submit]:nth-of-type(2) { + margin-left: 2.75em; + } + + &:not(:first-of-type) { + button[type=submit]:nth-of-type(2):last-of-type { + margin-right: 2.75em; + } + } + + &:last-of-type { + margin-bottom: 0.5em; + } +} + +.dimension-name { + font-weight: bold; + margin-left: 1em; +} + +ul.action-links { + list-style-type: none; + margin: 0; + padding: 0; +} + +ul.action-links { + li { + margin: 0; + padding: 0; + float: left; + display: inline-block; + width: 18em; + height: 10em; + overflow: hidden; + } + + li.error { + background-color: @color-critical; + color: @text-color-on-icinga-blue; + padding: 1em; + font-weight: bold; + text-align: center; + } + + a { + width: 100%; + height: 100%; + padding: 0.5em 1em; + display: block; + text-decoration: none; + color: @gray; + + .title { + margin-left: 5em; + font-weight: bold; + color: @link-color; + } + + p { + margin-left: 5em; + } + + i { + position: absolute; + font-size: 3em; + display: inline-block; + } + &:hover { + background: @tr-hover-color; + text-decoration: none; + } + } +} + +/** Form **/ + +.content form { + margin-bottom: 2em; +} + +form button[type=submit] { + margin-top: 0.5em; +} + +select { + width: 100%; +} @@ -0,0 +1,8 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +$this->provideHook('cube/Actions', 'Cube/MonitoringActions'); +$this->provideHook('cube/IcingaDbActions', 'Cube/IcingaDbActions'); + +$this->provideHook('icingadb/icingadbSupport'); |