diff options
Diffstat (limited to '')
53 files changed, 6416 insertions, 0 deletions
diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..d00642d --- /dev/null +++ b/.mailmap @@ -0,0 +1,9 @@ +Alexander A. Klimov <alexander.klimov@icinga.com> <alexander.klimov@netways.de> +Feu Mourek <feu.mourek@icinga.com> <feu.mourek@netways.de> +Feu Mourek <feu.mourek@icinga.com> <jennifer.mourek@icinga.com> +Florian Strohmaier <florian.strohmaier@icinga.com> <florian.strohmaier@me.com> +Michael Friedrich <michael.friedrich@icinga.com> <michael.friedrich@netways.de> +Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <sukhwinder33445@gmail.com> +Thomas Gelf <thomas.gelf@icinga.com> <thomas@gelf.net> +Yonas Habteab <yonas.habteab@icinga.com> <57616252+Yonas-net@users.noreply.github.com> +raviks789 <ravi.srinivasa@icinga.com> <33730024+raviks789@users.noreply.github.com> @@ -0,0 +1,14 @@ +Alexander A. Klimov <alexander.klimov@icinga.com> +Eric Lippmann <eric.lippmann@icinga.com> +Feu Mourek <feu.mourek@icinga.com> +Florian Strohmaier <florian.strohmaier@icinga.com> +Johannes Meyer <johannes.meyer@icinga.com> +Ken Jungclaus <lum33n@web.de> +Michael Friedrich <michael.friedrich@icinga.com> +Nicolai Buchwitz <nicolai.buchwitz@enda.eu> +Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> +Thomas Gelf <thomas.gelf@icinga.com> +Timm Ortloff <timm.ortloff@icinga.com> +Yonas Habteab <yonas.habteab@icinga.com> +raviks789 <ravi.srinivasa@icinga.com> +sant-swedge <simon.wedge@sant.ox.ac.uk> @@ -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..45fce7a --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# 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=main) +[![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 +------------------------- + +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..c41d846 --- /dev/null +++ b/application/controllers/HostsController.php @@ -0,0 +1,45 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Module\Cube\IcingaDb\IcingaDbHostStatusCube; +use Icinga\Module\Cube\Web\Controller; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; + +class HostsController extends Controller +{ + public function indexAction(): void + { + $this->createTabs()->activate('cube/hosts'); + + $this->renderCube(); + } + + protected function getCube(): IcingaDbCube + { + return new IcingaDbHostStatusCube(); + } + + public function completeAction(): void + { + $suggestions = new ObjectSuggestions(); + $suggestions->setModel(Host::class); + $suggestions->forRequest($this->getServerRequest()); + $this->getDocument()->add($suggestions); + } + + public function searchEditorAction(): void + { + $editor = $this->createSearchEditor( + Host::on($this->getDb()), + $this->preserveParams + ); + + $this->getDocument()->add($editor); + $this->setTitle($this->translate('Adjust Filter')); + } +} diff --git a/application/controllers/IdoHostsController.php b/application/controllers/IdoHostsController.php new file mode 100644 index 0000000..8648823 --- /dev/null +++ b/application/controllers/IdoHostsController.php @@ -0,0 +1,24 @@ +<?php + +// Icinga Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Module\Cube\Ido\IdoCube; +use Icinga\Module\Cube\Ido\IdoHostStatusCube; +use Icinga\Module\Cube\Web\IdoController; + +class IdoHostsController extends IdoController +{ + public function indexAction(): void + { + $this->createTabs()->activate('cube/hosts'); + + $this->renderCube(); + } + + protected function getCube(): IdoCube + { + return new IdoHostStatusCube(); + } +} diff --git a/application/controllers/IdoServicesController.php b/application/controllers/IdoServicesController.php new file mode 100644 index 0000000..f55e1d7 --- /dev/null +++ b/application/controllers/IdoServicesController.php @@ -0,0 +1,24 @@ +<?php + +// Icinga Cube Module | (c) 2022 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Module\Cube\Ido\IdoCube; +use Icinga\Module\Cube\Ido\IdoServiceStatusCube; +use Icinga\Module\Cube\Web\IdoController; + +class IdoServicesController extends IdoController +{ + public function indexAction(): void + { + $this->createTabs()->activate('cube/services'); + + $this->renderCube(); + } + + protected function getCube(): IdoCube + { + return new IdoServiceStatusCube(); + } +} 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..0914aa2 --- /dev/null +++ b/application/controllers/ServicesController.php @@ -0,0 +1,45 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Controllers; + +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Module\Cube\IcingaDb\IcingaDbServiceStatusCube; +use Icinga\Module\Cube\Web\Controller; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; + +class ServicesController extends Controller +{ + public function indexAction(): void + { + $this->createTabs()->activate('cube/services'); + + $this->renderCube(); + } + + protected function getCube(): IcingaDbCube + { + return new IcingaDbServiceStatusCube(); + } + + public function completeAction(): void + { + $suggestions = new ObjectSuggestions(); + $suggestions->setModel(Service::class); + $suggestions->forRequest($this->getServerRequest()); + $this->getDocument()->add($suggestions); + } + + public function searchEditorAction(): void + { + $editor = $this->createSearchEditor( + Service::on($this->getDb()), + $this->preserveParams + ); + + $this->getDocument()->add($editor); + $this->setTitle($this->translate('Adjust Filter')); + } +} diff --git a/application/forms/DimensionsForm.php b/application/forms/DimensionsForm.php new file mode 100644 index 0000000..fbfbbc7 --- /dev/null +++ b/application/forms/DimensionsForm.php @@ -0,0 +1,211 @@ +<?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\Notification; +use ipl\Html\Form; +use ipl\Html\Html; +use ipl\I18n\Translation; +use ipl\Web\Common\FormUid; +use ipl\Web\Url; +use ipl\Web\Widget\Icon; + +class DimensionsForm extends Form +{ + use FormUid; + use Translation; + + protected $defaultAttributes = [ + 'class' => 'icinga-controls', + 'name' => 'dimensions-form' + ]; + + /** + * @var Cube + */ + private $cube; + + /** + * @var Url + */ + private $url; + + /** + * Get the url + * + * @return Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the url + * + * @param mixed $url + */ + public function setUrl($url): self + { + $this->url = $url; + + return $this; + } + + public function setCube(Cube $cube) + { + $this->cube = $cube; + return $this; + } + + public function hasBeenSubmitted(): bool + { + // required to submit dimension controls and the selected dropdown option + return $this->hasBeenSent() && + ($this->getPressedSubmitElement() !== null || $this->getPopulatedValue('addDimension')); + } + + public function assemble() + { + $dimensions = $this->cube->listDimensions(); + $cnt = count($dimensions); + + if ($cnt < 3) { + $allDimensions = $this->cube->listAdditionalDimensions(); + + $this->addElement('select', 'addDimension', [ + 'options' => [null => $this->translate('+ Add a dimension')] + $allDimensions, + 'class' => 'autosubmit' + ]); + } + + $pos = 0; + foreach ($dimensions as $dimension) { + $this->addDimensionButtons($dimension, $pos++, $cnt); + } + + foreach ($this->cube->getSlices() as $key => $value) { + $this->addSlice($this->cube->getDimension($key), $value); + } + + $this->addElement($this->createUidElement()); + } + + protected function addSlice(Dimension $dimension, $value) + { + $sliceId = sha1($this->cube::SLICE_PREFIX . $dimension->getName()); + + $sliceFieldset = Html::tag('fieldset', ['class' => 'dimensions']); + + $btn = $this->createElement('submitButton', 'removeSlice_' . $sliceId, [ + 'label' => new Icon('trash'), + 'class' => 'dimension-control' + ]); + + $this->registerElement($btn); + $sliceFieldset->addHtml($btn); + + $sliceFieldset->addHtml(Html::tag( + 'span', + ['class' => 'dimension-name'], + sprintf('%s: %s = %s', $this->translate('Slice/Filter'), $dimension->getLabel(), $value) + )); + + $this->addHtml($sliceFieldset); + } + + protected function addDimensionButtons(Dimension $dimension, $pos, $total) + { + $dimensionId = sha1($dimension->getName()); + + $dimensionFieldset = Html::tag('fieldset', ['class' => 'dimensions']); + + $btn = $this->createElement('submitButton', 'removeDimension_' . $dimensionId, [ + 'label' => new Icon('trash'), + 'title' => sprintf($this->translate('Remove dimension "%s"'), $dimension->getLabel()), + 'class' => 'dimension-control' + ]); + + $this->registerElement($btn); + $dimensionFieldset->addHtml($btn); + + if ($pos > 0) { + $btn = $this->createElement('submitButton', 'moveDimensionUp_' . $dimensionId, [ + 'label' => new Icon('angle-double-up'), + 'title' => sprintf($this->translate('Move dimension "%s" up'), $dimension->getLabel()), + 'class' => 'dimension-control', + ]); + + $this->registerElement($btn); + $dimensionFieldset->addHtml($btn); + } + + if ($pos + 1 !== $total) { + $btn = $this->createElement('submitButton', 'moveDimensionDown_' . $dimensionId, [ + 'label' => new Icon('angle-double-down'), + 'title' => sprintf($this->translate('Move dimension "%s" down'), $dimension->getLabel()), + 'class' => 'dimension-control' + ]); + + $this->registerElement($btn); + $dimensionFieldset->addHtml($btn); + } + + $dimensionFieldset->addHtml(Html::tag('span', ['class' => 'dimension-name'], $dimension->getLabel())); + + $this->addHtml($dimensionFieldset); + } + + public function onSuccess() + { + $url = $this->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; + $pressedButtonName = $this->getPressedSubmitElement()->getName(); + + foreach ($this->cube->listDimensions() as $name => $_) { + $dimensionId = sha1($name); + + switch (true) { + case ($pressedButtonName === 'removeDimension_' . $dimensionId): + $this->cube->removeDimension($name); + $updateDimensions = true; + break 2; + case ($pressedButtonName === 'moveDimensionUp_' . $dimensionId): + $this->cube->moveDimensionUp($name); + $updateDimensions = true; + break 2; + case ($pressedButtonName === 'moveDimensionDown_' . $dimensionId): + $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) { + $slice = $this->cube::SLICE_PREFIX . $slice; + $sliceId = sha1($slice); + + if ($pressedButtonName === 'removeSlice_' . $sliceId) { + $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..8e0611f --- /dev/null +++ b/application/views/scripts/cube-details.phtml @@ -0,0 +1,12 @@ +<div class="controls"> +<?php if (! \Icinga\Module\Cube\Cube::isUsingIcingaDb()): ?> + <?= $this->tabs ?> +<?php endif ?> +<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..f46d74a --- /dev/null +++ b/application/views/scripts/cube-index.phtml @@ -0,0 +1,22 @@ +<div class="controls"> + <?php if (! $this->compact): ?> + <?php if (! \Icinga\Module\Cube\Cube::isUsingIcingaDb()): ?> + <?= $this->tabs ?> + <?php endif ?> + <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..bd9dd58 --- /dev/null +++ b/doc/01-About.md @@ -0,0 +1,56 @@ +# 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 +------------------------- + +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..d6f3271 --- /dev/null +++ b/doc/02-Installation.md @@ -0,0 +1,23 @@ +<!-- {% 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 note that [Icinga Web](https://icinga.com/docs/icinga-web) is required to run Icinga Cube +and if it is not already set up, it is best to do this first. + +The following steps will guide you through installing and setting up Icinga Cube. +<!-- {% else %} --> +<!-- {% if not icingaDocs %} --> + +## Installing the Package + +If the [repository](https://packages.icinga.com) is not configured yet, please add it first. +Then use your distribution's package manager to install the `icinga-cube` package +or install [from source](02-Installation.md.d/From-Source.md). +<!-- {% endif %} --><!-- {# end if not icingaDocs #} --> + +## Configuring Icinga Cube + +No additional steps are required to set up Icinga Cube and it is ready to use right after installation. +<!-- {% endif %} --><!-- {# end else if index #} --> diff --git a/doc/02-Installation.md.d/From-Source.md b/doc/02-Installation.md.d/From-Source.md new file mode 100644 index 0000000..92ac15e --- /dev/null +++ b/doc/02-Installation.md.d/From-Source.md @@ -0,0 +1,15 @@ +# Installing Icinga Cube from Source + +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.13.0) + +If you are using PostgreSQL, you need at least version 9.5 which provides the `ROLLUP` feature. +<!-- {% 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..2f3abe3 --- /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..b081c1d --- /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..1fb3c09 --- /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..91bb8f2 --- /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..4880cc3 --- /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..1e688f0 --- /dev/null +++ b/library/Cube/Cube.php @@ -0,0 +1,341 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Application\Modules\Module; +use Icinga\Exception\IcingaException; +use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Web\View; + +abstract class Cube +{ + /** @var ?string Prefix for slice params */ + public const SLICE_PREFIX = null; + + /** @var ?bool Whether the icingadb backend is in use */ + public const IS_USING_ICINGADB = null; + + /** @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(); + + /** + * Get whether the icingadb backend is in use + * + * @return bool + */ + public static function isUsingIcingaDb(): bool + { + return static::IS_USING_ICINGADB + ?? (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()); + } + + 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 $name => $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 ($this->hasDimension($name)) { + 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..e88a938 --- /dev/null +++ b/library/Cube/CubeRenderer.php @@ -0,0 +1,512 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Web\View; +use ipl\Stdlib\Filter; +use ipl\Web\Url; +use Generator; +use Icinga\Data\Tree\TreeNode; + +/** + * 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(); + + /** + * Get the severity sort columns + * + * @return Generator + */ + abstract protected function getSeveritySortColumns(): Generator; + + /** + * 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(); + + $results = $this->cube->fetchAll(); + + if (! empty($results) && $this->cube::isUsingIcingaDb()) { + $sortBy = $this->cube->getSortBy(); + if ($sortBy && $sortBy[0] === $this->cube::DIMENSION_SEVERITY_SORT_PARAM) { + $isSortDirDesc = isset($sortBy[1]) && $sortBy[1] !== 'asc'; + $results = $this->sortBySeverity($results, $isSortDirDesc); + } + } + + foreach ($results as $row) { + $htm .= $this->renderRow($row); + } + + return $htm . $this->closeDimensions() . $this->endContainer(); + } + + + /** + * Sort the results by severity + * + * @param $results array The fetched results + * @param $isSortDirDesc bool Whether the sort direction is descending + * + * @return Generator + */ + private function sortBySeverity(array $results, bool $isSortDirDesc): Generator + { + $perspective = end($this->dimensionOrder); + $resultsCount = count($results); + $tree = [new TreeNode()]; + + $prepareHeaders = function (array $tree, object $row): TreeNode { + $node = (new TreeNode()) + ->setValue($row); + $parent = end($tree); + $parent->appendChild($node); + + return $node; + }; + + $i = 0; + do { + $row = $results[$i]; + while ($row->$perspective === null) { + $tree[] = $prepareHeaders($tree, $row); + + if (! isset($results[++$i])) { + break; + } + + $row = $results[$i]; + } + + for (; $i < $resultsCount; $i++) { + $row = $results[$i]; + + $anyNull = false; + foreach ($this->dimensionOrder as $dimension) { + if ($row->$dimension === null) { + $anyNull = true; + array_pop($tree); + } + } + + if ($anyNull) { + break; + } + + $prepareHeaders($tree, $row); + } + } while ($i < $resultsCount); + + $nodes = function (TreeNode $node) use (&$nodes, $isSortDirDesc): Generator { + yield $node->getValue(); + $children = $node->getChildren(); + + uasort($children, function (TreeNode $a, TreeNode $b) use ($isSortDirDesc): int { + foreach ($this->getSeveritySortColumns() as $column) { + $comparison = $a->getValue()->$column <=> $b->getValue()->$column; + if ($comparison !== 0) { + return $comparison * ($isSortDirDesc ? -1 : 1); + } + } + + // $a and $b are equal in terms of $priorities. + return 0; + }); + + foreach ($children as $node) { + yield from $nodes($node); + } + }; + + return $nodes($tree[1]); + } + + 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) + { + $url = Url::fromPath($this->getDetailsBaseUrl()); + + if ($this->cube instanceof IcingaDbCube && $this->cube->hasBaseFilter()) { + /** @var Filter\Rule $baseFilter */ + $baseFilter = $this->cube->getBaseFilter(); + $url->setFilter($baseFilter); + } + + $urlParams = $url->getParams(); + + $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices()); + $urlParams->add('dimensions', DimensionParams::update($dimensions)->getParams()); + + foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) { + $urlParams->add($this->cube::SLICE_PREFIX . $dimensionName, $row->$dimensionName); + } + + foreach ($this->cube->getSlices() as $key => $val) { + $urlParams->add($this->cube::SLICE_PREFIX . $key, $val); + } + + return $url; + } + + protected function getSliceUrl($name, $row) + { + return $this->view->url() + ->setParam($this->cube::SLICE_PREFIX . $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..777f41b --- /dev/null +++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php @@ -0,0 +1,143 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\CubeRenderer; + +use Generator; +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; + + $next = $this->cube->getDimensionAfter($name); + if ($next && isset($this->summaries->{$next->getName()})) { + $sums = $this->summaries->{$next->getName()}; + } + + $severityClass = []; + if ($sums->hosts_unhandled_down > 0) { + $severityClass[] = 'critical'; + } elseif (isset($sums->hosts_unhandled_unreachable) && $sums->hosts_unhandled_unreachable > 0) { + $severityClass[] = 'unreachable'; + } + + if (empty($severityClass)) { + if ($sums->hosts_down > 0) { + $severityClass = ['critical', 'handled']; + } elseif (isset($sums->hosts_unreachable) && $sums->hosts_unreachable > 0) { + $severityClass = ['unreachable', 'handled']; + } else { + $severityClass[] = 'ok'; + } + } + + return array_merge($classes, $severityClass); + } + + public function renderFacts($facts) + { + $indent = str_repeat(' ', 3); + $parts = array(); + + if ($facts->hosts_unhandled_down > 0) { + $parts['critical'] = $facts->hosts_unhandled_down; + } + + if (isset($facts->hosts_unhandled_unreachable) && $facts->hosts_unhandled_unreachable > 0) { + $parts['unreachable'] = $facts->hosts_unhandled_unreachable; + } + + if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) { + $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down; + } + + if ( + isset($facts->hosts_unreachable, $facts->hosts_unhandled_unreachable) + && $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 + && (! isset($facts->hosts_unreachable) || $facts->hosts_cnt > $facts->hosts_unreachable) + ) { + $ok = $facts->hosts_cnt - $facts->hosts_down; + if (isset($facts->hosts_unreachable)) { + $ok -= $facts->hosts_unreachable; + } + + $parts['ok'] = $ok; + } + + $main = ''; + $sub = ''; + foreach ($parts as $class => $count) { + if ($count === 0) { + continue; + } + + 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'; + } + + protected function getSeveritySortColumns(): Generator + { + $columns = ['down', 'unreachable']; + foreach ($columns as $column) { + yield "hosts_unhandled_$column"; + } + + foreach ($columns as $column) { + yield "hosts_$column"; + } + } +} diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php new file mode 100644 index 0000000..f115742 --- /dev/null +++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php @@ -0,0 +1,149 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\CubeRenderer; + +use Generator; +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_unhandled_unknown > 0) { + $parts['unknown'] = $facts->services_unhandled_unknown; + } + + if ($facts->services_unhandled_warning > 0) { + $parts['warning'] = $facts->services_unhandled_warning; + } + + 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_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) { + $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown; + } + + 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_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 ($count === 0) { + continue; + } + + 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; + + $next = $this->cube->getDimensionAfter($name); + if ($next && isset($this->summaries->{$next->getName()})) { + $sums = $this->summaries->{$next->getName()}; + } + + if ($sums->services_unhandled_critical > 0) { + $severityClass[] = 'critical'; + } elseif ($sums->services_unhandled_unknown > 0) { + $severityClass[] = 'unknown'; + } elseif ($sums->services_unhandled_warning > 0) { + $severityClass[] = 'warning'; + } + + if (empty($severityClass)) { + if ($sums->services_critical > 0) { + $severityClass = ['critical', 'handled']; + } elseif ($sums->services_unknown > 0) { + $severityClass = ['unknown', 'handled']; + } elseif ($sums->services_warning > 0) { + $severityClass = ['warning', 'handled']; + } else { + $severityClass[] = 'ok'; + } + } + + return array_merge($classes, $severityClass); + } + + 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'; + } + + protected function getSeveritySortColumns(): Generator + { + $columns = ['critical', 'unknown', 'warning']; + foreach ($columns as $column) { + yield "services_unhandled_$column"; + } + + foreach ($columns as $column) { + yield "services_$column"; + } + } +} diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php new file mode 100644 index 0000000..071e934 --- /dev/null +++ b/library/Cube/Dimension.php @@ -0,0 +1,70 @@ +<?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(); + + /** + * Fetch label of this dimension + * + * @return string + */ + public function getLabel(); + + /** + * Add a label + * + * @param string $label + * + * @return $this + */ + public function addLabel(string $label); + + /** + * Set the label for the dimension + * + * @param string $label + * + * @return $this + */ + public function setLabel(string $label); + + /** + * 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..c0205bb --- /dev/null +++ b/library/Cube/DimensionParams.php @@ -0,0 +1,85 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube; + +use Icinga\Web\Url; +use ipl\Stdlib\Str; + +class DimensionParams +{ + /** + * @var array Raw dimensions + */ + protected $dimensions = []; + + /** + * @var string encoded dimensions separated by coma + */ + protected $params; + + 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; + } + + public static function fromString($dimensions) + { + return static::fromArray(Str::trimSplit($dimensions)); + } + + /** + * @param $dimension + * + * @return $this + */ + public function add($dimension) + { + if (! empty($dimension)) { + $this->dimensions[] = $dimension; + } + + return $this; + } + + /** + * Overwrite dimensions + * + * @param $dimensions + * + * @return DimensionParams + */ + 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..34a395c --- /dev/null +++ b/library/Cube/IcingaDb/CustomVariableDimension.php @@ -0,0 +1,166 @@ +<?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\Common\Auth; +use Icinga\Module\Icingadb\Model\CustomvarFlat; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Sql\Expression; +use ipl\Stdlib\Filter; + +class CustomVariableDimension implements Dimension +{ + use Auth; + + /** @var string Prefix for host custom variable */ + public const HOST_PREFIX = 'host.vars.'; + + /** @var string Prefix for service custom variable */ + public const SERVICE_PREFIX = 'service.vars.'; + + /** @var ?string variable source name */ + protected $sourceName; + + /** @var ?string Variable name without prefix */ + protected $varName; + + /** @var string Variable name with prefix */ + protected $name; + + protected $label; + + protected $wantNull = false; + + public function __construct($name) + { + if (preg_match('/^(host|service)\.vars\.(.*)/', $name, $matches)) { + $this->sourceName = $matches[1]; + $this->varName = $matches[2]; + } + + $this->name = $name; + } + + /** + * Get the variable name without prefix + * + * @return string + */ + public function getVarName(): string + { + return $this->varName ?? $this->getName(); + } + + /** + * Get the variable source name + * + * @return ?string + */ + public function getSourceName(): ?string + { + return $this->sourceName; + } + + public function getName() + { + return $this->name; + } + + public function getLabel() + { + return $this->label ?: $this->getName(); + } + + public function setLabel($label) + { + $this->label = $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([$this->createCustomVarAlias(), 'flatvalue']); + + if ($this->wantNull) { + return new Expression("COALESCE($expression, '-')"); + } + + return $expression; + } + + public function addToCube(Cube $cube) + { + /** @var IcingaDbCube $cube */ + $innerQuery = $cube->innerQuery(); + $sourceTable = $this->getSourceName() ?? $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', $this->getVarName())); + + // Values might not be unique (wildcard dimensions) + $subQueryModelAlias = $subQuery->getResolver()->getAlias($subQuery->getModel()); + $subQuery->getSelectBase()->groupBy([ + $subQueryModelAlias . '.flatname', // Required by postgres, if there are any custom variable protections + $subQueryModelAlias . '.flatvalue', + 'object_id' + ]); + + $this->applyRestrictions($subQuery); + + $subQueryAlias = $cube->getDb()->quoteIdentifier([$this->createCustomVarAlias()]); + $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue'); + + $sourceIdPath = '.id'; + if ($innerQuery->getModel() instanceof Service && $sourceTable === 'host') { + $sourceIdPath = '.host_id'; + } + + $innerQuery->getSelectBase()->join( + [$subQueryAlias => $subQuery->assembleSelect()], + [ + $subQueryAlias . '.object_id = ' + . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . $sourceIdPath + ] + ); + } + + protected function createCustomVarAlias(): string + { + return implode('_', ['c', $this->getSourceName(), $this->getVarName()]); + } +} diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php new file mode 100644 index 0000000..44c7619 --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbCube.php @@ -0,0 +1,338 @@ +<?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 Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Orm\Common\SortUtil; +use ipl\Orm\Query; +use ipl\Sql\Adapter\Pgsql; +use ipl\Sql\Expression; +use ipl\Sql\Select; +use ipl\Stdlib\BaseFilter; + +abstract class IcingaDbCube extends Cube +{ + use Auth; + use BaseFilter; + use Database; + + public const SLICE_PREFIX = 'slice.'; + public const IS_USING_ICINGADB = true; + + /** @var bool Whether to show problems only */ + protected $problemsOnly = false; + + /** @var string Sort param used to sort dimensions by value */ + public const DIMENSION_VALUE_SORT_PARAM = 'value'; + + /** @var string Sort param used to sort dimensions by severity */ + public const DIMENSION_SEVERITY_SORT_PARAM = 'severity'; + + /** @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; + + /** @var array The sort order of dimensions, column as key and direction as value */ + protected $sortBy; + + 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; + } + + /** + * Set whether to show problems only + * + * @param bool $problemOnly + * + * @return $this + */ + public function problemsOnly(bool $problemOnly = true): self + { + $this->problemsOnly = $problemOnly; + + return $this; + } + + + /** + * Get whether to show problems only + * + * @return bool + */ + public function isProblemsOnly(): bool + { + return $this->problemsOnly; + } + + /** + * Fetch the host variable dimensions + * + * @return array + */ + public function fetchHostVariableDimensions(): array + { + $query = Host::on($this->getDb()) + ->with('customvar_flat') + ->columns('customvar_flat.flatname') + ->orderBy('customvar_flat.flatname'); + + $this->applyRestrictions($query); + + $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->customvar_flat->flatname); + $name = strtolower($name); + $dimensions[CustomVariableDimension::HOST_PREFIX . $name] = 'Host ' . $name; + } + + return $dimensions; + } + + /** + * Fetch the service variable dimensions + * + * @return array + */ + public function fetchServiceVariableDimensions(): array + { + $query = Service::on($this->getDb()) + ->with('customvar_flat') + ->columns('customvar_flat.flatname') + ->orderBy('customvar_flat.flatname'); + + $this->applyRestrictions($query); + + $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->customvar_flat->flatname); + $name = strtolower($name); + $dimensions[CustomVariableDimension::SERVICE_PREFIX . $name] = 'Service ' . $name; + } + + return $dimensions; + } + + /** + * Set sort by columns + * + * @param ?string $sortBy + * + * @return $this + */ + public function sortBy(?string $sortBy): self + { + if (empty($sortBy)) { + return $this; + } + + $this->sortBy = SortUtil::createOrderBy($sortBy)[0]; + + return $this; + } + + /** + * Get sort by columns + * + * @return ?array Column as key and direction as value + */ + public function getSortBy(): ?array + { + return $this->sortBy; + } + + /** + * 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(); + $select = $query->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)) { + $select->where( + $dimension->getColumnExpression($this) . ' = ?', + $this->slices[$name] + ); + } else { + $columns[$quotedDimension] = $dimension->getColumnExpression($this); + } + } + + $select->columns($columns); + + $this->applyRestrictions($query); + if ($this->hasBaseFilter()) { + $query->filter($this->getBaseFilter()); + } + } + + 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 = []; + $orderBy = []; + $sortBy = $this->getSortBy(); + foreach ($this->listColumns() as $column) { + $quotedColumn = $this->getDb()->quoteIdentifier([$column]); + $columns[$quotedColumn] = 'rollup.' . $quotedColumn; + + if ($this->hasDimension($column)) { + $orderBy["($quotedColumn IS NOT NULL)"] = null; + + $sortDir = 'ASC'; + if ($sortBy && self::DIMENSION_VALUE_SORT_PARAM === $sortBy[0]) { + $sortDir = $sortBy[1] ?? 'ASC'; + } + + $orderBy[$quotedColumn] = $sortDir; + } + } + + return (new Select()) + ->from(['rollup' => $rollupQuery]) + ->columns($columns) + ->orderBy($orderBy); + } + + /** + * 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..14e083f --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php @@ -0,0 +1,80 @@ +<?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\Host; +use Icinga\Module\Icingadb\Model\HoststateSummary; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Str; + +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', + ]; + } + + public function createDimension($name) + { + $this->registerAvailableDimensions(); + + if (isset($this->availableDimensions[$name])) { + return clone $this->availableDimensions[$name]; + } + + return new CustomVariableDimension($name); + } + + public function listAvailableDimensions() + { + return $this->fetchHostVariableDimensions(); + } + + 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..ac59de2 --- /dev/null +++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php @@ -0,0 +1,94 @@ +<?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\Service; +use Icinga\Module\Icingadb\Model\ServicestateSummary; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Str; + +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() + { + return array_merge( + $this->fetchServiceVariableDimensions(), + $this->fetchHostVariableDimensions() + ); + } + + 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..df45497 --- /dev/null +++ b/library/Cube/Ido/CustomVarDimension.php @@ -0,0 +1,146 @@ +<?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(); + } + + public function setLabel($label) + { + $this->varLabel = $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 IdoCube $cube */ + $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..7ad609d --- /dev/null +++ b/library/Cube/Ido/IdoCube.php @@ -0,0 +1,219 @@ +<?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; + + public const IS_USING_ICINGADB = false; + + /** + * Add a specific named dimension + * + * Right now these are just custom vars, we might support group memberships + * or other properties in future + * + * @param string $name + * + * @return $this + */ + public function addDimensionByName($name): self + { + if (count($this->filterProtectedCustomvars([$name])) === 1) { + $this->addDimension($this->createDimension($name)); + } + + return $this; + } + + /** + * 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..3f79dc8 --- /dev/null +++ b/library/Cube/Ido/IdoHostStatusCube.php @@ -0,0 +1,97 @@ +<?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); + } + + /** + * 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'); + + $dimensions = $this->filterProtectedCustomvars($this->db()->fetchCol($select)); + $keys = array_map('strtolower', $dimensions); + + return array_combine($keys, $dimensions); + } + + 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..a403645 --- /dev/null +++ b/library/Cube/Ido/IdoServiceStatusCube.php @@ -0,0 +1,97 @@ +<?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'); + + $dimensions = $this->filterProtectedCustomvars($this->db()->fetchCol($select)); + $keys = array_map('strtolower', $dimensions); + + return array_combine($keys, $dimensions); + } + + 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; + } + + 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..1fbd05d --- /dev/null +++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php @@ -0,0 +1,48 @@ +<?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; +use ipl\Stdlib\Filter; +use ipl\Web\Url; + +class IcingaDbActions extends IcingaDbActionsHook +{ + public function createActionLinks(IcingaDbCube $cube) + { + $type = 'host'; + if ($cube instanceof IcingaDbServiceStatusCube) { + $type = 'service'; + } + + $filter = Filter::all(); + if ($cube->hasBaseFilter()) { + $filter->add($cube->getBaseFilter()); + } + + foreach ($cube->getSlices() as $dimension => $slice) { + $filter->add(Filter::equal($dimension, $slice)); + } + + $url = Url::fromPath('icingadb/' . $type . 's'); + $url->setFilter($filter); + + if ($type === 'host') { + $this->addActionLink( + $url, + t('Show hosts status'), + t('This shows all matching hosts and their current state in Icinga DB Web'), + 'server' + ); + } else { + $this->addActionLink( + $url, + 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..4b84fac --- /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[] = self::renderErrorItem($e, $view); + } + + foreach ($hook->getActionLinks()->getLinks() as $link) { + $html[] = '<li>' . $link->render($view) . '</li>'; + } + } + + if (empty($html)) { + $html[] = self::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..028a744 --- /dev/null +++ b/library/Cube/Web/Controller.php @@ -0,0 +1,297 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +namespace Icinga\Module\Cube\Web; + +use Icinga\Module\Cube\DimensionParams; +use Icinga\Module\Cube\Forms\DimensionsForm; +use Icinga\Module\Cube\Hook\IcingaDbActionsHook; +use Icinga\Module\Cube\IcingaDb\CustomVariableDimension; +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Web\Control\ProblemToggle; +use ipl\Html\FormElement\CheckboxElement; +use ipl\Html\HtmlString; +use ipl\Stdlib\Filter; +use ipl\Stdlib\Str; +use ipl\Web\Compat\CompatController; +use ipl\Web\Compat\SearchControls; +use ipl\Web\Control\SortControl; +use ipl\Web\Filter\QueryString; +use ipl\Web\Url; +use ipl\Web\Widget\Tabs; + +abstract class Controller extends CompatController +{ + use SearchControls; + use Database; + use Auth; + + /** @var string[] Preserved params for searchbar and search editor controls */ + protected $preserveParams = [ + 'dimensions', + 'showSettings', + 'wantNull', + 'problems', + 'sort' + ]; + + /** @var Filter\Rule Filter from query string parameters */ + private $filter; + + /** + * Return this controllers' cube + * + * @return IcingaDbCube + */ + abstract protected function getCube(): IcingaDbCube; + + /** + * Get the filter created from query string parameters + * + * @return Filter\Rule + */ + public function getFilter(): Filter\Rule + { + if ($this->filter === null) { + $this->filter = QueryString::parse((string) $this->params); + } + + return $this->filter; + } + + public function detailsAction(): void + { + $cube = $this->prepareCube(); + $this->getTabs()->add('details', [ + 'label' => $this->translate('Cube details'), + 'url' => $this->getRequest()->getUrl() + ])->activate('details'); + + $cube->setBaseFilter($this->getFilter()); + + $this->setTitle($cube->getSlicesLabel()); + $this->view->links = IcingaDbActionsHook::renderAll($cube); + + $this->addContent( + HtmlString::create($this->view->render('/cube-details.phtml')) + ); + } + + protected function renderCube(): void + { + $cube = $this->prepareCube(); + $this->setTitle(sprintf( + $this->translate('Cube: %s'), + $cube->getPathLabel() + )); + + $this->params->shift('format'); + $showSettings = $this->params->shift('showSettings'); + + $query = $cube->innerQuery(); + $problemsOnly = (bool) $this->params->shift('problems', false); + $problemToggle = (new ProblemToggle($problemsOnly ?: null)) + ->setIdProtector([$this->getRequest(), 'protectId']) + ->on(ProblemToggle::ON_SUCCESS, function (ProblemToggle $form) { + /** @var CheckboxElement $problems */ + $problems = $form->getElement('problems'); + if (! $problems->isChecked()) { + $this->redirectNow(Url::fromRequest()->remove('problems')); + } else { + $this->redirectNow(Url::fromRequest()->setParam('problems')); + } + })->handleRequest($this->getServerRequest()); + + $this->addControl($problemToggle); + + $sortControl = SortControl::create([ + IcingaDbCube::DIMENSION_VALUE_SORT_PARAM => t('Value'), + IcingaDbCube::DIMENSION_SEVERITY_SORT_PARAM . ' desc' => t('Severity'), + ]); + + $this->params->shift($sortControl->getSortParam()); + $cube->sortBy($sortControl->getSort()); + $this->addControl($sortControl); + + $searchBar = $this->createSearchBar( + $query, + $this->preserveParams + ); + + if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { + if ($searchBar->hasBeenSubmitted()) { + $filter = $this->getFilter(); + } else { + $this->addControl($searchBar); + $this->sendMultipartUpdate(); + return; + } + } else { + $filter = $searchBar->getFilter(); + } + + if ($problemsOnly) { + $filter = Filter::all($filter, Filter::equal('state.is_problem', true)); + } + + $cube->setBaseFilter($filter); + $cube->problemsOnly($problemsOnly); + + $this->addControl($searchBar); + + if (count($cube->listDimensions()) > 0) { + $this->view->cube = $cube; + } else { + $showSettings = true; + } + + $this->view->url = Url::fromRequest() + ->onlyWith($this->preserveParams) + ->setFilter($searchBar->getFilter()); + + if ($showSettings) { + $form = (new DimensionsForm()) + ->setUrl($this->view->url) + ->setCube($cube) + ->on(DimensionsForm::ON_SUCCESS, function ($form) { + $this->redirectNow($form->getRedirectUrl()); + }) + ->handleRequest($this->getServerRequest()); + + $this->view->form = $form; + } else { + $this->setAutorefreshInterval(15); + } + + $this->addContent( + HtmlString::create($this->view->render('/cube-index.phtml')) + ); + + if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { + $this->sendMultipartUpdate(); + } + } + + private function prepareCube(): IcingaDbCube + { + $cube = $this->getCube(); + $cube->chooseFacts(array_keys($cube->getAvailableFactColumns())); + + $dimensions = DimensionParams::fromString( + $this->params->shift('dimensions', '') + )->getDimensions(); + + if ($this->hasLegacyDimensionParams($dimensions)) { + $this->transformLegacyDimensionParamsAndRedirect($dimensions); + } + + $wantNull = $this->params->shift('wantNull'); + foreach ($dimensions as $dimension) { + $cube->addDimensionByName($dimension); + if ($wantNull) { + $cube->getDimension($dimension)->wantNull(); + } + + $sliceParamWithPrefix = rawurlencode($cube::SLICE_PREFIX . $dimension); + + if ($this->params->has($sliceParamWithPrefix)) { + $this->preserveParams[] = $sliceParamWithPrefix; + $cube->slice($dimension, $this->params->shift($sliceParamWithPrefix)); + } + } + + return $cube; + } + + /** + * Get whether the given dimension param is legacy dimension param + * + * @param string $dimensionParam + * + * @return bool + */ + private function isLegacyDimensionParam(string $dimensionParam): bool + { + return ! Str::startsWith($dimensionParam, CustomVariableDimension::HOST_PREFIX) + && ! Str::startsWith($dimensionParam, CustomVariableDimension::SERVICE_PREFIX); + } + + /** + * Get whether the dimensions contain legacy dimension + * + * @param array $dimensions + * + * @return bool + */ + private function hasLegacyDimensionParams(array $dimensions): bool + { + foreach ($dimensions as $dimension) { + if ($this->isLegacyDimensionParam($dimension)) { + return true; + } + } + + return false; + } + + /** + * Transform legacy dimension and slice params and redirect + * + * This adds the new prefix to params and then redirects so that the new URL contains the prefixed params + * Slices are prefixed to differ filter and slice params + * + * @param array $legacyDimensions + */ + private function transformLegacyDimensionParamsAndRedirect(array $legacyDimensions): void + { + $dimensions = []; + $slices = []; + + $dimensionPrefix = CustomVariableDimension::HOST_PREFIX; + if ($this->getRequest()->getControllerName() === 'services') { + $dimensionPrefix = CustomVariableDimension::SERVICE_PREFIX; + } + + foreach ($legacyDimensions as $param) { + $newParam = $param; + if ($this->isLegacyDimensionParam($param)) { + $newParam = $dimensionPrefix . $param; + } + + $slice = $this->params->shift($param); + if ($slice) { + $slices[IcingaDbCube::SLICE_PREFIX . $newParam] = $slice; + } + + $dimensions[] = $newParam; + } + + $this->redirectNow( + Url::fromRequest() + ->setParam('dimensions', DimensionParams::fromArray($dimensions)->getParams()) + ->addParams($slices) + ->without($legacyDimensions) + ); + } + + public function createTabs(): Tabs + { + $params = Url::fromRequest() + ->onlyWith($this->preserveParams) + ->getParams() + ->toString(); + + return $this->getTabs() + ->add('cube/hosts', [ + 'label' => $this->translate('Hosts'), + 'url' => 'cube/hosts' . ($params === '' ? '' : '?' . $params) + ]) + ->add('cube/services', [ + 'label' => $this->translate('Services'), + 'url' => 'cube/services' . ($params === '' ? '' : '?' . $params) + ]); + } +} diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php new file mode 100644 index 0000000..a9feec9 --- /dev/null +++ b/library/Cube/Web/IdoController.php @@ -0,0 +1,198 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2019 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\IcingaDb\CustomVariableDimension; +use Icinga\Module\Cube\IcingaDb\IcingaDbCube; +use Icinga\Module\Cube\Ido\IdoCube; +use ipl\Stdlib\Str; +use ipl\Web\Compat\CompatController; +use ipl\Web\Url; +use ipl\Web\Widget\Tabs; + +abstract class IdoController extends CompatController +{ + /** + * Return this controllers' cube + * + * @return IdoCube + */ + abstract protected function getCube(): IdoCube; + + public function detailsAction(): void + { + $cube = $this->prepareCube(); + + $this->getTabs()->add('details', [ + 'label' => $this->translate('Cube details'), + 'url' => $this->getRequest()->getUrl() + ])->activate('details'); + + $this->view->title = $cube->getSlicesLabel(); + + $this->view->links = ActionLinks::renderAll($cube, $this->view); + + $this->render('cube-details', null, true); + } + + protected function renderCube(): void + { + $this->params->shift('format'); + $showSettings = $this->params->shift('showSettings'); + + $cube = $this->prepareCube(); + + $this->view->title = sprintf( + $this->translate('Cube: %s'), + $cube->getPathLabel() + ); + + if (count($cube->listDimensions()) > 0) { + $this->view->cube = $cube; + } else { + $showSettings = true; + } + + $this->view->url = Url::fromRequest(); + if ($showSettings) { + $form = (new DimensionsForm()) + ->setUrl($this->view->url) + ->setCube($cube) + ->setUrl(Url::fromRequest()) + ->on(DimensionsForm::ON_SUCCESS, function ($form) { + $this->redirectNow($form->getRedirectUrl()); + }) + ->handleRequest($this->getServerRequest()); + + $this->view->form = $form; + } else { + $this->setAutorefreshInterval(15); + } + + $this->render('cube-index', null, true); + } + + private function prepareCube(): IdoCube + { + $cube = $this->getCube(); + $cube->chooseFacts(array_keys($cube->getAvailableFactColumns())); + + $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions(); + + $resolved = $this->params->shift('resolved', false); + + if ( + ! $resolved + && Module::exists('icingadb') + && $this->hasIcingadbDimensionParams($vars) + ) { + $this->transformIcingadbDimensionParamsAndRedirect($vars); + } elseif ($resolved) { + $this->redirectNow(Url::fromRequest()->without('resolved')); + } + + $wantNull = $this->params->shift('wantNull'); + + foreach ($vars as $var) { + $cube->addDimensionByName($var); + if ($wantNull) { + $cube->getDimension($var)->wantNull(); + } + } + + foreach ($this->params->toArray() as $param) { + $cube->slice(rawurldecode($param[0]), rawurldecode($param[1])); + } + + return $cube; + } + + /** + * Get whether the dimensions contain icingadb dimension + * + * @param array $dimensions + * + * @return bool + */ + private function hasIcingadbDimensionParams(array $dimensions): bool + { + foreach ($dimensions as $dimension) { + if ( + Str::startsWith($dimension, CustomVariableDimension::HOST_PREFIX) + || Str::startsWith($dimension, CustomVariableDimension::SERVICE_PREFIX) + ) { + return true; + } + } + + return false; + } + + /** + * Transform icingadb dimension and slice params and redirect + * + * This remove the new icingadb prefix from params and remove sort, problems-only, filter params + * + * @param array $icingadbDimensions + */ + private function transformIcingadbDimensionParamsAndRedirect(array $icingadbDimensions): void + { + $dimensions = []; + $slices = []; + $toRemoveSlices = []; + + $prefix = CustomVariableDimension::HOST_PREFIX; + if ($this->getRequest()->getControllerName() === 'ido-services') { + $prefix = CustomVariableDimension::SERVICE_PREFIX; + } + + foreach ($icingadbDimensions as $param) { + $newParam = $param; + if (strpos($param, $prefix) !== false) { + $newParam = substr($param, strlen($prefix)); + } + + $slice = $this->params->shift(IcingaDbCube::SLICE_PREFIX . $param); + if ($slice) { + $slices[$newParam] = $slice; + $toRemoveSlices[] = IcingaDbCube::SLICE_PREFIX . $param; + } + + $dimensions[] = $newParam; + } + + $icingadbParams = array_merge( + $icingadbDimensions, + $toRemoveSlices, + array_keys($this->params->toArray(false)) + ); + + $this->redirectNow( + Url::fromRequest() + ->setParam('dimensions', DimensionParams::fromArray($dimensions)->getParams()) + ->addParams($slices) + ->addParams(['resolved' => true]) + ->without($icingadbParams) + ); + } + + public function createTabs(): Tabs + { + $params = Url::fromRequest()->getParams()->toString(); + + return $this->getTabs() + ->add('cube/hosts', [ + 'label' => $this->translate('Hosts'), + 'url' => 'cube/hosts' . ($params === '' ? '' : '?' . $params) + ]) + ->add('cube/services', [ + 'label' => $this->translate('Services'), + 'url' => 'cube/services' . ($params === '' ? '' : '?' . $params) + ]); + } +} diff --git a/module.info b/module.info new file mode 100644 index 0000000..64dfc12 --- /dev/null +++ b/module.info @@ -0,0 +1,10 @@ +Name: Cube +Version: 1.3.2 +Requires: + Libraries: icinga-php-library (>=0.13.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/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..0c37e72 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,1031 @@ +parameters: + ignoreErrors: + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Controllers\\\\IndexController\\:\\:indexAction\\(\\) has no return type specified\\.$#" + count: 1 + path: application/controllers/IndexController.php + + - + message: "#^Cannot call method getName\\(\\) on ipl\\\\Html\\\\Contract\\\\FormSubmitElement\\|null\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addDimensionButtons\\(\\) has no return type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addDimensionButtons\\(\\) has parameter \\$pos with no type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addDimensionButtons\\(\\) has parameter \\$total with no type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addSlice\\(\\) has no return type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:addSlice\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:assemble\\(\\) has no return type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:setCube\\(\\) has no return type specified\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Forms\\\\DimensionsForm\\:\\:\\$url \\(ipl\\\\Web\\\\Url\\) does not accept mixed\\.$#" + count: 1 + path: application/forms/DimensionsForm.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:addDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:chooseFacts\\(\\) has parameter \\$facts with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:fetchAll\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has parameter \\$array with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has parameter \\$pos1 with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:flipPositions\\(\\) has parameter \\$pos2 with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimension\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimensionAfter\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimensionAfter\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getDimensionsLabel\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getPathLabel\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getSlices\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getSlicesLabel\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasDimension\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasFact\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasFact\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasSlice\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:hasSlice\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listAdditionalDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listAvailableDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listColumns\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listDimensionsUpTo\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listDimensionsUpTo\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listFacts\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:listSlices\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionDown\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionDown\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionUp\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:moveDimensionUp\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:reOrderDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:reOrderDimensions\\(\\) has parameter \\$positions with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:registerAvailableDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:removeDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:removeDimension\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:slice\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:slice\\(\\) has parameter \\$key with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:slice\\(\\) has parameter \\$value with no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$availableDimensions \\(array\\<string, Icinga\\\\Module\\\\Cube\\\\Dimension\\>\\) does not accept array\\<int\\|string, Icinga\\\\Module\\\\Cube\\\\Dimension\\>\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$chosenFacts type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$renderer has no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:\\$slices has no type specified\\.$#" + count: 1 + path: library/Cube/Cube.php + + - + message: "#^Access to undefined constant Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:DIMENSION_SEVERITY_SORT_PARAM\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Call to an undefined method Icinga\\\\Module\\\\Cube\\\\Cube\\:\\:getSortBy\\(\\)\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimension\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimension\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsForRow\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsForRow\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsUpFrom\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsUpFrom\\(\\) has parameter \\$dimension with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:beginDimensionsUpFrom\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimension\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsDownTo\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsDownTo\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsForRow\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:closeDimensionsForRow\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:extractFacts\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDetailsUrl\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDetailsUrl\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDetailsUrl\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClassString\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClassString\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClassString\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClasses\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getIndent\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getIndent\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getLevel\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getLevel\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getSliceUrl\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getSliceUrl\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:getSliceUrl\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:initialize\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:isOuterDimension\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:isOuterDimension\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:render\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderFacts\\(\\) has parameter \\$facts with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderRow\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:renderRow\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:sortBySeverity\\(\\) has parameter \\$results with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$dimensionLevels type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$dimensionOrder type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$dimensions type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$facts has no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$reversedDimensions type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\:\\:\\$started has no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$class with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$count with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\HostStatusCubeRenderer\\:\\:renderFacts\\(\\) has parameter \\$facts with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/HostStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:getDimensionClasses\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$class with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:makeBadgeHtml\\(\\) has parameter \\$count with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:renderDimensionLabel\\(\\) has parameter \\$row with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\CubeRenderer\\\\ServiceStatusCubeRenderer\\:\\:renderFacts\\(\\) has parameter \\$facts with no type specified\\.$#" + count: 1 + path: library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:add\\(\\) has parameter \\$dimension with no type specified\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromArray\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromArray\\(\\) has parameter \\$dimensions with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromString\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromString\\(\\) has parameter \\$dimensions with no type specified\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:fromUrl\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:getDimensions\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:update\\(\\) has parameter \\$dimensions with no type specified\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\DimensionParams\\:\\:\\$dimensions type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/DimensionParams.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Hook\\\\ActionsHook\\:\\:makeUrl\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Hook/ActionsHook.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Hook\\\\IcingaDbActionsHook\\:\\:makeUrl\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Hook/IcingaDbActionsHook.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\CustomVariableDimension\\:\\:__construct\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/CustomVariableDimension.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\CustomVariableDimension\\:\\:\\$label has no type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/CustomVariableDimension.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\CustomVariableDimension\\:\\:\\$wantNull has no type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/CustomVariableDimension.php + + - + message: "#^Cannot access property \\$customvar_flat on mixed\\.$#" + count: 2 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:fetchAll\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:fetchHostVariableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:fetchServiceVariableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:getObjectsFilter\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:getSortBy\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:prepareFullQuery\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:prepareRollupQuery\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Parameter \\#1 \\$filter of method ipl\\\\Orm\\\\Query\\:\\:filter\\(\\) expects ipl\\\\Stdlib\\\\Filter\\\\Rule, ipl\\\\Stdlib\\\\Filter\\\\Rule\\|null given\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:\\$objectsFilter has no type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:\\$sortBy \\(array\\) does not accept mixed\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbCube\\:\\:\\$sortBy type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbCube.php + + - + message: "#^Cannot access property \\$host on mixed\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbHostStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbHostStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbHostStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbHostStatusCube\\:\\:listAvailableDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbHostStatusCube.php + + - + message: "#^Cannot access property \\$host_name on mixed\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php + + - + message: "#^Cannot access property \\$service_name on mixed\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbServiceStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\IcingaDb\\\\IcingaDbServiceStatusCube\\:\\:listAvailableDimensions\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/IcingaDb/IcingaDbServiceStatusCube.php + + - + message: "#^Call to an undefined method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:joinLeft\\(\\)\\.$#" + count: 1 + path: library/Cube/Ido/CustomVarDimension.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:__construct\\(\\) has parameter \\$varName with no type specified\\.$#" + count: 1 + path: library/Cube/Ido/CustomVarDimension.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:safeVarname\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Ido/CustomVarDimension.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:safeVarname\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Ido/CustomVarDimension.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\CustomVarDimension\\:\\:\\$type \\(string\\) does not accept string\\|null\\.$#" + count: 1 + path: library/Cube/Ido/CustomVarDimension.php + + - + message: "#^Call to an undefined method Icinga\\\\Data\\\\ConnectionInterface\\:\\:getResource\\(\\)\\.$#" + count: 1 + path: library/Cube/Ido/DataView/Hoststatus.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DataView\\\\Hoststatus\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/DataView/Hoststatus.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:chooseFacts\\(\\) has parameter \\$facts with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/DbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/DbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:fetchAll\\(\\) should return array but returns array\\|null\\.$#" + count: 1 + path: library/Cube/Ido/DbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/DbCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:setDbName\\(\\) has parameter \\$name with no type specified\\.$#" + count: 1 + path: library/Cube/Ido/DbCube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:\\$factColumns type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/DbCube.php + + - + message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Cannot access offset 'host'\\|'service' on array\\|stdClass\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Cannot call method getDbAdapter\\(\\) on mixed\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Cannot call method getRestrictions\\(\\) on Icinga\\\\User\\|null\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:filterProtectedCustomvars\\(\\) should return array\\<string\\> but returns array\\|false\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:getIdoVersion\\(\\) should return string but returns string\\|false\\|null\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:getMonitoringRestriction\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Parameter \\#1 \\$connection of method Icinga\\\\Module\\\\Cube\\\\Ido\\\\DbCube\\:\\:setConnection\\(\\) expects Icinga\\\\Data\\\\Db\\\\DbConnection, mixed given\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Parameter \\#2 \\$subject of function preg_split expects string, mixed given\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:\\$availableFacts type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoCube\\:\\:\\$idoVersion \\(string\\) does not accept string\\|false\\|null\\.$#" + count: 1 + path: library/Cube/Ido/IdoCube.php + + - + message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:getSelectQuery\\(\\)\\.$#" + count: 2 + path: library/Cube/Ido/IdoHostStatusCube.php + + - + message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:requireColumn\\(\\)\\.$#" + count: 1 + path: library/Cube/Ido/IdoHostStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoHostStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/IdoHostStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoHostStatusCube\\:\\:listAvailableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/IdoHostStatusCube.php + + - + message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:getSelectQuery\\(\\)\\.$#" + count: 2 + path: library/Cube/Ido/IdoServiceStatusCube.php + + - + message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:requireColumn\\(\\)\\.$#" + count: 1 + path: library/Cube/Ido/IdoServiceStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoServiceStatusCube\\:\\:getAvailableFactColumns\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/IdoServiceStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\IdoServiceStatusCube\\:\\:listAvailableDimensions\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/IdoServiceStatusCube.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/Query/HoststatusQuery.php + + - + message: "#^Property Icinga\\\\Module\\\\Cube\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/Query/HoststatusQuery.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:__call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:columns\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:columns\\(\\) has parameter \\$cols with no type specified\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:columns\\(\\) has parameter \\$correlationName with no type specified\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:reset\\(\\) has no return type specified\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Ido\\\\ZfSelectWrapper\\:\\:reset\\(\\) has parameter \\$part with no type specified\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{Zend_Db_Select, string\\} given\\.$#" + count: 1 + path: library/Cube/Ido/ZfSelectWrapper.php + + - + message: "#^Parameter \\#1 \\$rule of method ipl\\\\Stdlib\\\\Filter\\\\Chain\\:\\:add\\(\\) expects ipl\\\\Stdlib\\\\Filter\\\\Rule, ipl\\\\Stdlib\\\\Filter\\\\Rule\\|null given\\.$#" + count: 1 + path: library/Cube/ProvidedHook/Cube/IcingaDbActions.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\Controller\\:\\:hasLegacyDimensionParams\\(\\) has parameter \\$dimensions with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Web/Controller.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\Controller\\:\\:transformLegacyDimensionParamsAndRedirect\\(\\) has parameter \\$legacyDimensions with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Web/Controller.php + + - + message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, false given\\.$#" + count: 1 + path: library/Cube/Web/Controller.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\IdoController\\:\\:hasIcingadbDimensionParams\\(\\) has parameter \\$dimensions with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Web/IdoController.php + + - + message: "#^Method Icinga\\\\Module\\\\Cube\\\\Web\\\\IdoController\\:\\:transformIcingadbDimensionParamsAndRedirect\\(\\) has parameter \\$icingadbDimensions with no value type specified in iterable type array\\.$#" + count: 1 + path: library/Cube/Web/IdoController.php + + - + message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, false given\\.$#" + count: 1 + path: library/Cube/Web/IdoController.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..82abfa0 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,27 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + + checkFunctionNameCase: true + checkInternalClassCaseSensitivity: true + treatPhpDocTypesAsCertain: false + + paths: + - application + - library + + scanDirectories: + - vendor + + ignoreErrors: + - + messages: + - '#Unsafe usage of new static\(\)#' + - '#. but return statement is missing#' + reportUnmatched: false + + universalObjectCratesClasses: + - Icinga\Web\View + - ipl\Orm\Model diff --git a/public/css/module.less b/public/css/module.less new file mode 100644 index 0000000..88ba80e --- /dev/null +++ b/public/css/module.less @@ -0,0 +1,354 @@ +// 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, &:focus { + .rounded-corners(); + background-color: @icinga-blue; + color: @text-color-on-icinga-blue; + } + + &:focus { + outline: 3px solid fade(@icinga-blue, 50%); + outline-offset: 1px; + } + } + + 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.50em; + } + + &:not(:first-of-type) { + button[type=submit]:nth-of-type(2):last-of-type { + margin-right: 2.50em; + } + } + + &: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; + min-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 { + display: inline-block; + 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; + + button[type=submit] { + margin-top: 0.5em; + } + + select { + width: 100%; + } +} + +.controls .icinga-form .toggle-switch { + margin-top: .25em; + margin-bottom: .25em; +} @@ -0,0 +1,48 @@ +<?php + +// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2 + +use Icinga\Module\Cube\Cube; + +$this->provideHook('cube/Actions', 'Cube/MonitoringActions'); +$this->provideHook('cube/IcingaDbActions', 'Cube/IcingaDbActions'); + +$this->provideHook('icingadb/icingadbSupport'); + +if (! Cube::isUsingIcingaDb()) { + $this->addRoute('cube/hosts', new Zend_Controller_Router_Route_Static( + 'cube/hosts', + [ + 'controller' => 'ido-hosts', + 'action' => 'index', + 'module' => 'cube' + ] + )); + + $this->addRoute('cube/hosts/details', new Zend_Controller_Router_Route_Static( + 'cube/hosts/details', + [ + 'controller' => 'ido-hosts', + 'action' => 'details', + 'module' => 'cube' + ] + )); + + $this->addRoute('cube/services', new Zend_Controller_Router_Route_Static( + 'cube/services', + [ + 'controller' => 'ido-services', + 'action' => 'index', + 'module' => 'cube' + ] + )); + + $this->addRoute('cube/services/details', new Zend_Controller_Router_Route_Static( + 'cube/services/details', + [ + 'controller' => 'ido-services', + 'action' => 'details', + 'module' => 'cube' + ] + )); +} |