summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:42:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:42:52 +0000
commitda0f8427204ad57aad08059906df0ea10a7ccf31 (patch)
treeccda92692e296a50dac104945f62b4402d5e4447
parentInitial commit. (diff)
downloadicingaweb2-module-cube-upstream.tar.xz
icingaweb2-module-cube-upstream.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--LICENSE339
-rw-r--r--README.md64
-rw-r--r--application/controllers/HostsController.php30
-rw-r--r--application/controllers/IndexController.php15
-rw-r--r--application/controllers/ServicesController.php30
-rw-r--r--application/forms/DimensionsForm.php206
-rw-r--r--application/views/scripts/cube-details.phtml10
-rw-r--r--application/views/scripts/cube-index.phtml20
-rw-r--r--configuration.php5
-rw-r--r--doc/01-About.md58
-rw-r--r--doc/02-Installation.md235
-rw-r--r--doc/02-Installation.md.d/01-Amazon-Linux.md3
-rw-r--r--doc/02-Installation.md.d/02-CentOS.md3
-rw-r--r--doc/02-Installation.md.d/03-Debian.md3
-rw-r--r--doc/02-Installation.md.d/04-RHEL.md3
-rw-r--r--doc/02-Installation.md.d/05-SLES.md3
-rw-r--r--doc/02-Installation.md.d/06-Ubuntu.md3
-rw-r--r--doc/02-Installation.md.d/07-From-Source.md3
-rw-r--r--doc/img/cube_action-links.pngbin0 -> 33474 bytes
-rw-r--r--doc/img/cube_director.pngbin0 -> 45415 bytes
-rw-r--r--doc/img/cube_move-up.pngbin0 -> 49941 bytes
-rw-r--r--doc/img/cube_simple.pngbin0 -> 44872 bytes
-rw-r--r--doc/img/cube_slice.pngbin0 -> 24887 bytes
-rw-r--r--library/Cube/Cube.php323
-rw-r--r--library/Cube/CubeRenderer.php406
-rw-r--r--library/Cube/CubeRenderer/HostStatusCubeRenderer.php105
-rw-r--r--library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php126
-rw-r--r--library/Cube/Dimension.php52
-rw-r--r--library/Cube/DimensionParams.php87
-rw-r--r--library/Cube/Hook/ActionsHook.php99
-rw-r--r--library/Cube/Hook/IcingaDbActionsHook.php125
-rw-r--r--library/Cube/IcingaDb/CustomVariableDimension.php119
-rw-r--r--library/Cube/IcingaDb/IcingaDbCube.php196
-rw-r--r--library/Cube/IcingaDb/IcingaDbHostStatusCube.php100
-rw-r--r--library/Cube/IcingaDb/IcingaDbServiceStatusCube.php109
-rw-r--r--library/Cube/Ido/CustomVarDimension.php158
-rw-r--r--library/Cube/Ido/DataView/Hoststatus.php17
-rw-r--r--library/Cube/Ido/DbCube.php298
-rw-r--r--library/Cube/Ido/IdoCube.php198
-rw-r--r--library/Cube/Ido/IdoHostStatusCube.php112
-rw-r--r--library/Cube/Ido/IdoServiceStatusCube.php113
-rw-r--r--library/Cube/Ido/Query/HoststatusQuery.php47
-rw-r--r--library/Cube/Ido/ZfSelectWrapper.php77
-rw-r--r--library/Cube/ProvidedHook/Cube/IcingaDbActions.php41
-rw-r--r--library/Cube/ProvidedHook/Cube/MonitoringActions.php53
-rw-r--r--library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php11
-rw-r--r--library/Cube/Web/ActionLink.php103
-rw-r--r--library/Cube/Web/ActionLinks.php115
-rw-r--r--library/Cube/Web/Controller.php115
-rw-r--r--library/Cube/Web/IdoController.php24
-rw-r--r--module.info10
-rw-r--r--public/css/module.less343
-rw-r--r--run.php8
53 files changed, 4723 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..30110ac
--- /dev/null
+++ b/README.md
@@ -0,0 +1,64 @@
+# Icinga Cube
+
+[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/)
+![Build Status](https://github.com/icinga/icingaweb2-module-cube/workflows/PHP%20Tests/badge.svg?branch=master)
+[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-cube.svg)](https://github.com/Icinga/icingaweb2-module-cube)
+
+![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png)
+
+The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icingaweb2)
+module. It currently shows host and service statistics (total count, health) grouped by
+various custom variables in multiple dimensions.
+
+![Cube - Overview](doc/img/cube_simple.png)
+
+It will be your new best friend in case you are running a large environment and
+want to get a quick answers to questions like:
+
+* Which project uses how many servers per environment at which location/site?
+ * Who occupies most servers?
+ * How many of those are used in production?
+ * Which project has only development and test boxes?
+* Which operating system is used for which project and in which environment?
+ * Do we still have Debian Lenny?
+ * Which projects are to blame for this?
+ * Do we have applications where the operating systems used differ in staging
+ and production?
+* Which project uses which operating system version for which application?
+ * Which projects have homogeneous environments?
+ * Which projects are at a consistent patch level?
+ * How many RHEL 6 variants (6.1, 6.2, 6.3...) do we use?
+ * Who is running the oldest ones? In production?
+* Which projects are still using physical servers in which environment?
+
+For Businessmen - Drill and Slice
+---------------------------------
+
+Get answers to your questions. Quick and fully autonomous, using the cube
+requires no technical skills. Choose amongst all available dimensions and rotate
+the Cube to fit your needs.
+
+![Cube - Configure Dimensions](doc/img/cube_move-up.png)
+
+Want to drill down? Choose a slice and get your answers:
+
+![Cube - Configure Dimensions](doc/img/cube_slice.png)
+
+All facts configured for systems monitored by [Icinga](https://www.icinga.com/)
+can be used for your research.
+
+For Icinga Director users
+-------------------------
+
+![Cube - Action Links](doc/img/cube_action-links.png)
+
+In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director),
+in addition to the multi-selection/edit feature the cube provides a nice way to
+modify multiple hosts at once.
+
+![Cube - Director multi-edit](doc/img/cube_director.png)
+
+Installation
+------------
+
+To install Icinga Cube see [Installation](https://icinga.com/docs/icinga-cube/latest/doc/02-Installation/).
diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php
new file mode 100644
index 0000000..c3e5374
--- /dev/null
+++ b/application/controllers/HostsController.php
@@ -0,0 +1,30 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Cube\IcingaDb\IcingaDbHostStatusCube;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Cube\Web\IdoController;
+
+class HostsController extends IdoController
+{
+ public function indexAction()
+ {
+ $this->createTabs()->activate('cube/hosts');
+
+ $this->renderCube();
+ }
+
+ protected function getCube()
+ {
+ if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
+ return new IcingaDbHostStatusCube();
+ }
+
+ return new IdoHostStatusCube();
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..082fda3
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,15 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Web\Controller;
+
+class IndexController extends Controller
+{
+ public function indexAction()
+ {
+ $this->redirectNow('cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString()));
+ }
+}
diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php
new file mode 100644
index 0000000..ec08bbb
--- /dev/null
+++ b/application/controllers/ServicesController.php
@@ -0,0 +1,30 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Cube\IcingaDb\IcingaDbServiceStatusCube;
+use Icinga\Module\Cube\Ido\IdoServiceStatusCube;
+use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Cube\Web\IdoController;
+
+class ServicesController extends IdoController
+{
+ public function indexAction()
+ {
+ $this->createTabs()->activate('cube/services');
+
+ $this->renderCube();
+ }
+
+ protected function getCube()
+ {
+ if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
+ return new IcingaDbServiceStatusCube();
+ }
+
+ return new IdoServiceStatusCube();
+ }
+}
diff --git a/application/forms/DimensionsForm.php b/application/forms/DimensionsForm.php
new file mode 100644
index 0000000..c4fcf73
--- /dev/null
+++ b/application/forms/DimensionsForm.php
@@ -0,0 +1,206 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Forms;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+class DimensionsForm extends Form
+{
+ /**
+ * @var Cube
+ */
+ private $cube;
+
+ public function setCube(Cube $cube)
+ {
+ $this->cube = $cube;
+ return $this;
+ }
+
+ public function createElements(array $formData)
+ {
+ $cube = $this->cube;
+ $dimensions = $cube->listDimensions();
+ $cnt = count($dimensions);
+
+ if ($cnt < 3) {
+ $allDimensions = $cube->listAdditionalDimensions();
+
+ $this->addElement('select', 'addDimension', [
+ 'multiOptions' => [null => $this->translate('+ Add a dimension')] + $allDimensions,
+ 'decorators' => ['ViewHelper'],
+ 'class' => 'autosubmit'
+ ]);
+ }
+
+ $pos = 0;
+ foreach ($dimensions as $dimension) {
+ $this->addDimensionButtons($dimension, $pos++, $cnt);
+ }
+
+ foreach ($cube->getSlices() as $key => $value) {
+ $this->addSlice($this->cube->getDimension($key), $value);
+ }
+
+ $this->addAttribs(['class' => 'icinga-controls']);
+ }
+
+ protected function addSlice(Dimension $dimension, $value)
+ {
+ $view = $this->getView();
+
+ $sliceId = sha1($dimension->getName());
+ $this->addElement('button', 'removeSlice_' . $sliceId, [
+ 'label' => $view->icon('cancel'),
+ 'decorators' => ['ViewHelper'],
+ 'value' => $dimension->getName(),
+ 'type' => 'submit',
+ 'escape' => false,
+ 'class' => 'dimension-control'
+ ]);
+
+ $label = $view->escape(
+ sprintf(
+ '%s: %s = %s',
+ $view->translate('Slice/Filter'),
+ $dimension->getLabel(),
+ $value
+ )
+ );
+
+ $this->addElement('note', 'slice_' . $sliceId, [
+ 'class' => 'dimension-name',
+ 'value' => '<span class="dimension-name">' . $label . '</span>',
+ 'decorators' => ['ViewHelper']
+ ]);
+
+ $this->addDisplayGroup(
+ [
+ 'removeSlice_' . $sliceId,
+ 'slice_' . $sliceId,
+ ],
+ $dimension->getName(),
+ [
+ 'class' => 'dimensions',
+ 'decorators' => [
+ 'FormElements',
+ 'Fieldset'
+ ]
+ ]
+ );
+ }
+
+ protected function addDimensionButtons(Dimension $dimension, $pos, $total)
+ {
+ $view = $this->getView();
+ $dimensionId = sha1($dimension->getName());
+
+ $this->addElement('note', 'dimension_' . $dimensionId, [
+ 'class' => 'dimension-name',
+ 'value' => '<span class="dimension-name">' . $view->escape($dimension->getLabel()) . '</span>',
+ 'decorators' => ['ViewHelper']
+ ]);
+
+ $this->addElement('button', 'removeDimension_' . $dimensionId, [
+ 'label' => $view->icon('cancel'),
+ 'decorators' => ['ViewHelper'],
+ 'title' => sprintf($this->translate('Remove dimension "%s"'), $dimension->getLabel()),
+ 'value' => $dimension->getName(),
+ 'type' => 'submit',
+ 'escape' => false,
+ 'class' => 'dimension-control'
+ ]);
+
+ if ($pos > 0) {
+ $this->addElement('button', 'moveDimensionUp_' . $dimensionId, [
+ 'label' => $view->icon('angle-double-up'),
+ 'decorators' => ['ViewHelper'],
+ 'title' => sprintf($this->translate('Move dimension "%s" up'), $dimension->getLabel()),
+ 'value' => $dimension->getName(),
+ 'type' => 'submit',
+ 'escape' => false,
+ 'class' => 'dimension-control'
+ ]);
+ }
+
+ if ($pos + 1 !== $total) {
+ $this->addElement('button', 'moveDimensionDown_' . $dimensionId, [
+ 'label' => $view->icon('angle-double-down'),
+ 'decorators' => ['ViewHelper'],
+ 'title' => sprintf($this->translate('Move dimension "%s" down'), $dimension->getLabel()),
+ 'value' => $dimension->getName(),
+ 'type' => 'submit',
+ 'escape' => false,
+ 'class' => 'dimension-control'
+ ]);
+ }
+
+ $this->addDisplayGroup(
+ [
+ 'removeDimension_' . $dimensionId,
+ 'moveDimensionUp_' . $dimensionId,
+ 'moveDimensionDown_' . $dimensionId,
+ 'dimension_' . $dimensionId,
+ ],
+ $dimensionId,
+ [
+ 'class' => 'dimensions',
+ 'decorators' => [
+ 'FormElements',
+ 'Fieldset'
+ ]
+ ]
+ );
+ }
+
+ public function onSuccess()
+ {
+ $url = $this->getRequest()->getUrl();
+
+ if ($dimension = $this->getValue('addDimension')) {
+ $url->setParam('dimensions', DimensionParams::fromUrl($url)->add($dimension)->getParams());
+ Notification::success($this->translate('New dimension has been added'));
+ } else {
+ $updateDimensions = false;
+ foreach ($this->cube->listDimensions() as $name => $_) {
+ $dimensionId = sha1($name);
+
+ switch (true) {
+ case ($el = $this->getElement('removeDimension_' . $dimensionId)) && $el->isChecked():
+ $this->cube->removeDimension($name);
+ $updateDimensions = true;
+ break 2;
+ case ($el = $this->getElement('moveDimensionUp_' . $dimensionId)) && $el->isChecked():
+ $this->cube->moveDimensionUp($name);
+ $updateDimensions = true;
+ break 2;
+ case ($el = $this->getElement('moveDimensionDown_' . $dimensionId)) && $el->isChecked():
+ $this->cube->moveDimensionDown($name);
+ $updateDimensions = true;
+ break 2;
+ }
+ }
+
+ if ($updateDimensions) {
+ $dimensions = array_merge(array_keys($this->cube->listDimensions()), $this->cube->listSlices());
+ $url->setParam('dimensions', DimensionParams::update($dimensions)->getParams());
+ } else {
+ foreach ($this->cube->listSlices() as $slice) {
+ $sliceId = sha1($slice);
+
+ if (($el = $this->getElement('removeSlice_' . $sliceId)) && $el->isChecked()) {
+ $url->getParams()->remove(rawurlencode($slice));
+ }
+ }
+ }
+ }
+
+ $this->setRedirectUrl($url);
+ }
+}
diff --git a/application/views/scripts/cube-details.phtml b/application/views/scripts/cube-details.phtml
new file mode 100644
index 0000000..b47bf9b
--- /dev/null
+++ b/application/views/scripts/cube-details.phtml
@@ -0,0 +1,10 @@
+<div class="controls">
+<?= $this->tabs ?>
+<h1><?= $this->escape($this->title) ?></h1>
+</div>
+
+<div class="content">
+ <ul class="action-links">
+ <?= $this->links ?>
+ </ul>
+</div>
diff --git a/application/views/scripts/cube-index.phtml b/application/views/scripts/cube-index.phtml
new file mode 100644
index 0000000..e45bcde
--- /dev/null
+++ b/application/views/scripts/cube-index.phtml
@@ -0,0 +1,20 @@
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs ?>
+ <h1><?= $this->escape($this->title) ?></h1>
+ <?php if ($this->form && ! $this->compact): ?>
+ <?= $this->qlink('Hide settings', $this->url()->without('showSettings'), null, ['icon' => 'wrench']) ?>
+ <?php else: ?>
+ <?= $this->qlink('Show settings', $this->url()->with('showSettings', true), null, ['icon' => 'wrench']) ?>
+ <?php endif ?>
+ <?php endif ?>
+</div>
+
+<div class="content">
+ <?php if ($this->form && ! $this->compact): ?>
+ <?= $this->form ?>
+ <?php endif ?>
+ <?php if ($this->cube): ?>
+ <?= $this->cube->render($this) ?>
+ <?php endif ?>
+</div>
diff --git a/configuration.php b/configuration.php
new file mode 100644
index 0000000..eaf1130
--- /dev/null
+++ b/configuration.php
@@ -0,0 +1,5 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+$this->menuSection(N_('Reporting'))->add($this->translate('Cube'))->setUrl('cube/hosts')->setPriority(10);
diff --git a/doc/01-About.md b/doc/01-About.md
new file mode 100644
index 0000000..c07095a
--- /dev/null
+++ b/doc/01-About.md
@@ -0,0 +1,58 @@
+# Icinga Cube
+
+The Icinga Cube is a tiny but useful [Icinga Web](https://github.com/Icinga/icingaweb2)
+module. It currently shows host and service statistics (total count, health) grouped by
+various custom variables in multiple dimensions.
+
+![Cube - Overview](img/cube_simple.png)
+
+It will be your new best friend in case you are running a large environment and
+want to get a quick answers to questions like:
+
+* Which project uses how many servers per environment at which location/site?
+ * Who occupies most servers?
+ * How many of those are used in production?
+ * Which project has only development and test boxes?
+* Which operating system is used for which project and in which environment?
+ * Do we still have Debian Lenny?
+ * Which projects are to blame for this?
+ * Do we have applications where the operating systems used differ in staging
+ and production?
+* Which project uses which operating system version for which application?
+ * Which projects have homogeneous environments?
+ * Which projects are at a consistent patch level?
+ * How many RHEL 6 variants (6.1, 6.2, 6.3...) do we use?
+ * Who is running the oldest ones? In production?
+* Which projects are still using physical servers in which environment?
+
+For Businessmen - Drill and Slice
+---------------------------------
+
+Get answers to your questions. Quick and fully autonomous, using the cube
+requires no technical skills. Choose amongst all available dimensions and rotate
+the Cube to fit your needs.
+
+![Cube - Configure Dimensions](img/cube_move-up.png)
+
+Want to drill down? Choose a slice and get your answers:
+
+![Cube - Configure Dimensions](img/cube_slice.png)
+
+All facts configured for systems monitored by [Icinga](https://www.icinga.com/)
+can be used for your research.
+
+For Icinga Director users
+-------------------------
+
+![Cube - Action Links](img/cube_action-links.png)
+
+In case you are using the [Icinga Director](https://github.com/Icinga/icingaweb2-module-director),
+in addition to the multi-selection/edit feature the cube provides a nice way to
+modify multiple hosts at once.
+
+![Cube - Director multi-edit](img/cube_director.png)
+
+Installation
+------------
+
+To install Icinga Cube see [Installation](02-Installation.md).
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
new file mode 100644
index 0000000..ef094bd
--- /dev/null
+++ b/doc/02-Installation.md
@@ -0,0 +1,235 @@
+<!-- {% if index %} -->
+# Installing Icinga Cube
+
+The recommended way to install Icinga Cube is to use prebuilt packages for
+all supported platforms from our official release repository.
+Please follow the steps listed for your target operating system,
+which guide you through setting up the repository and installing Icinga Cube.
+
+<!-- {% else %} -->
+<!-- {% if not from_source %} -->
+## Adding Icinga Package Repository
+
+The recommended way to install Icinga Cube is to use prebuilt packages from our official release repository.
+
+!!! tip
+
+ If you install Icinga Cube on a node that has Icinga 2, Icinga DB or Icinga Web installed via packages,
+ proceed to [installing the Icinga Cube package](#installing-icinga-cube-package) as
+ the repository is already configured.
+
+Here's how to add the official release repository:
+
+<!-- {% if amazon_linux %} -->
+<!-- {% if not icingaDocs %} -->
+### Amazon Linux 2 Repository
+<!-- {% endif %} -->
+!!! info
+
+ A paid repository subscription is required for Amazon Linux 2 repositories. Get more information on
+ [icinga.com/subscription](https://icinga.com/subscription).
+
+ Don't forget to fill in the username and password section with appropriate credentials in the local .repo file.
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+wget https://packages.icinga.com/subscription/amazon/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo
+```
+<!-- {% endif %} -->
+
+<!-- {% if centos %} -->
+<!-- {% if not icingaDocs %} -->
+### CentOS Repository
+<!-- {% endif %} -->
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+wget https://packages.icinga.com/centos/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo
+```
+<!-- {% endif %} -->
+
+<!-- {% if debian %} -->
+<!-- {% if not icingaDocs %} -->
+### Debian Repository
+<!-- {% endif %} -->
+
+```bash
+apt-get update
+apt-get -y install apt-transport-https wget gnupg
+
+wget -O - https://packages.icinga.com/icinga.key | apt-key add -
+
+DIST=$(awk -F"[)(]+" '/VERSION=/ {print $2}' /etc/os-release); \
+ echo "deb https://packages.icinga.com/debian icinga-${DIST} main" > \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+ echo "deb-src https://packages.icinga.com/debian icinga-${DIST} main" >> \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+
+apt-get update
+```
+<!-- {% endif %} -->
+
+<!-- {% if rhel %} -->
+<!-- {% if not icingaDocs %} -->
+### RHEL Repository
+<!-- {% endif %} -->
+!!! info
+
+ A paid repository subscription is required for RHEL repositories. Get more information on
+ [icinga.com/subscription](https://icinga.com/subscription).
+
+ Don't forget to fill in the username and password section with appropriate credentials in the local .repo file.
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+wget https://packages.icinga.com/subscription/rhel/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo
+```
+<!-- {% endif %} -->
+
+<!-- {% if sles %} -->
+<!-- {% if not icingaDocs %} -->
+### SLES Repository
+<!-- {% endif %} -->
+!!! info
+
+ A paid repository subscription is required for SLES repositories. Get more information on
+ [icinga.com/subscription](https://icinga.com/subscription).
+
+ Don't forget to fill in the username and password section with appropriate credentials in the local .repo file.
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+
+zypper ar https://packages.icinga.com/subscription/sles/ICINGA-release.repo
+zypper ref
+```
+<!-- {% endif %} -->
+
+<!-- {% if ubuntu %} -->
+<!-- {% if not icingaDocs %} -->
+### Ubuntu Repository
+<!-- {% endif %} -->
+
+```bash
+apt-get update
+apt-get -y install apt-transport-https wget gnupg
+
+wget -O - https://packages.icinga.com/icinga.key | apt-key add -
+
+. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi; \
+ echo "deb https://packages.icinga.com/ubuntu icinga-${DIST} main" > \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+ echo "deb-src https://packages.icinga.com/ubuntu icinga-${DIST} main" >> \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+
+apt-get update
+```
+<!-- {% endif %} -->
+
+## Installing Icinga Cube Package
+
+Use your distribution's package manager to install the `icinga-cube` package as follows:
+
+<!-- {% if amazon_linux %} -->
+<!-- {% if not icingaDocs %} -->
+#### Amazon Linux 2
+<!-- {% endif %} -->
+```bash
+yum install icinga-cube
+```
+<!-- {% endif %} -->
+
+<!-- {% if centos %} -->
+<!-- {% if not icingaDocs %} -->
+#### CentOS
+<!-- {% endif %} -->
+!!! info
+
+ Note that installing Icinga Cube is only supported on CentOS 7 as CentOS 8 is EOL.
+
+```bash
+yum install icinga-cube
+```
+<!-- {% endif %} -->
+
+<!-- {% if debian or ubuntu %} -->
+<!-- {% if not icingaDocs %} -->
+#### Debian / Ubuntu
+<!-- {% endif %} -->
+```bash
+apt-get install icinga-cube
+```
+<!-- {% endif %} -->
+
+<!-- {% if rhel %} -->
+#### RHEL 8 or Later
+
+```bash
+dnf install icinga-cube
+```
+
+#### RHEL 7
+
+```bash
+yum install icinga-cube
+```
+<!-- {% endif %} -->
+
+<!-- {% if sles %} -->
+<!-- {% if not icingaDocs %} -->
+#### SLES
+<!-- {% endif %} -->
+```bash
+zypper install icinga-cube
+```
+<!-- {% endif %} -->
+
+<!-- {% else %} --><!-- {# end if not from_source #} -->
+<!-- {% if not icingaDocs %} -->
+## Installing Icinga Cube from Source
+<!-- {% endif %} -->
+
+Please see the Icinga Web documentation on
+[how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source.
+Make sure you use `cube` as the module name. The following requirements must also be met.
+
+### Requirements
+
+* PHP (≥7.2)
+* [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9)
+* [Icinga DB Web](https://github.com/Icinga/icingadb-web) (≥1.0)
+* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.9)
+
+If you are using PostgreSQL, you need at least 9.5 which provides the `ROLLUP` feature.
+<!-- {% endif %} --><!-- {# end else if not from_source #} -->
+
+## Configuring Icinga Cube
+
+<!-- {% if not from_source %} -->
+The Icinga Web PHP framework is required to configure and run the Icinga Cube.
+Package installations of `icinga-cube` already set up the necessary dependencies.
+
+If Icinga Web has not been installed or set up before,
+you have completed the instructions here and can proceed to
+<!-- {% if amazon_linux %} -->
+[install the web server on Amazon Linux](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/06-Amazon-Linux/#install-the-web-server),
+<!-- {% endif %} -->
+<!-- {% if centos %} -->
+[install the web server on CentOS](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/03-CentOS/#install-the-web-server),
+<!-- {% endif %} -->
+<!-- {% if debian %} -->
+[install the web server on Debian](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/01-Debian/#install-the-web-server),
+<!-- {% endif %} -->
+<!-- {% if rhel %} -->
+[install the web server on RHEL](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/04-RHEL/#install-the-web-server),
+<!-- {% endif %} -->
+<!-- {% if sles %} -->
+[install the web server on SLES](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/05-SLES/#install-the-web-server),
+<!-- {% endif %} -->
+<!-- {% if ubuntu %} -->
+[install the web server on Ubuntu](https://icinga.com/docs/icinga-web-2/latest/doc/02-Installation/02-Ubuntu/#install-the-web-server),
+<!-- {% endif %} -->
+which will then take you to the web-based setup wizard, which also allows you to enable the Icinga Cube.
+<!-- {% endif %} --><!-- {# end if not from_source #} -->
+
+For Icinga Web setups already running, log in to Icinga Web with a privileged user and enable the Icinga Cube.
+<!-- {% endif %} --><!-- {# end else if index #} -->
diff --git a/doc/02-Installation.md.d/01-Amazon-Linux.md b/doc/02-Installation.md.d/01-Amazon-Linux.md
new file mode 100644
index 0000000..30340b1
--- /dev/null
+++ b/doc/02-Installation.md.d/01-Amazon-Linux.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube on Amazon Linux
+<!-- {% set amazon_linux = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/02-CentOS.md b/doc/02-Installation.md.d/02-CentOS.md
new file mode 100644
index 0000000..aaaaf1e
--- /dev/null
+++ b/doc/02-Installation.md.d/02-CentOS.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube on CentOS
+<!-- {% set centos = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/03-Debian.md b/doc/02-Installation.md.d/03-Debian.md
new file mode 100644
index 0000000..22c9603
--- /dev/null
+++ b/doc/02-Installation.md.d/03-Debian.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube on Debian
+<!-- {% set debian = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/04-RHEL.md b/doc/02-Installation.md.d/04-RHEL.md
new file mode 100644
index 0000000..fb6f738
--- /dev/null
+++ b/doc/02-Installation.md.d/04-RHEL.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube on RHEL
+<!-- {% set rhel = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/05-SLES.md b/doc/02-Installation.md.d/05-SLES.md
new file mode 100644
index 0000000..eb7bba7
--- /dev/null
+++ b/doc/02-Installation.md.d/05-SLES.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube on SLES
+<!-- {% set sles = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/06-Ubuntu.md b/doc/02-Installation.md.d/06-Ubuntu.md
new file mode 100644
index 0000000..0a4cbf2
--- /dev/null
+++ b/doc/02-Installation.md.d/06-Ubuntu.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube on Ubuntu
+<!-- {% set ubuntu = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/07-From-Source.md b/doc/02-Installation.md.d/07-From-Source.md
new file mode 100644
index 0000000..a5cb8d1
--- /dev/null
+++ b/doc/02-Installation.md.d/07-From-Source.md
@@ -0,0 +1,3 @@
+# Installing Icinga Cube from Source
+<!-- {% set from_source = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/img/cube_action-links.png b/doc/img/cube_action-links.png
new file mode 100644
index 0000000..79542e9
--- /dev/null
+++ b/doc/img/cube_action-links.png
Binary files differ
diff --git a/doc/img/cube_director.png b/doc/img/cube_director.png
new file mode 100644
index 0000000..3fc3730
--- /dev/null
+++ b/doc/img/cube_director.png
Binary files differ
diff --git a/doc/img/cube_move-up.png b/doc/img/cube_move-up.png
new file mode 100644
index 0000000..980a503
--- /dev/null
+++ b/doc/img/cube_move-up.png
Binary files differ
diff --git a/doc/img/cube_simple.png b/doc/img/cube_simple.png
new file mode 100644
index 0000000..0690d59
--- /dev/null
+++ b/doc/img/cube_simple.png
Binary files differ
diff --git a/doc/img/cube_slice.png b/doc/img/cube_slice.png
new file mode 100644
index 0000000..3aff4db
--- /dev/null
+++ b/doc/img/cube_slice.png
Binary files differ
diff --git a/library/Cube/Cube.php b/library/Cube/Cube.php
new file mode 100644
index 0000000..b307869
--- /dev/null
+++ b/library/Cube/Cube.php
@@ -0,0 +1,323 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Exception\IcingaException;
+use Icinga\Web\View;
+
+abstract class Cube
+{
+ /** @var array<string, Dimension> Available dimensions */
+ protected $availableDimensions;
+
+ /** @var array Fact names */
+ protected $chosenFacts;
+
+ /** @var Dimension[] */
+ protected $dimensions = array();
+
+ protected $slices = array();
+
+ protected $renderer;
+
+ abstract public function fetchAll();
+
+ public function removeDimension($name)
+ {
+ unset($this->dimensions[$name]);
+ unset($this->slices[$name]);
+ return $this;
+ }
+
+ /**
+ * @return CubeRenderer
+ * @throws IcingaException
+ */
+ public function getRenderer()
+ {
+ throw new IcingaException('Got no cube renderer');
+ }
+
+ public function getPathLabel()
+ {
+ $dimensions = $this->getDimensionsLabel();
+ $slices = $this->getSlicesLabel();
+ $parts = array();
+ if ($dimensions !== null) {
+ $parts[] = $dimensions;
+ }
+
+ if ($slices !== null) {
+ $parts[] = $slices;
+ }
+
+ return implode(', ', $parts);
+ }
+
+ public function getDimensionsLabel()
+ {
+ $dimensions = $this->listDimensions();
+ if (empty($dimensions)) {
+ return null;
+ }
+
+ return implode(' -> ', array_map(function ($d) {
+ return $d->getLabel();
+ }, $dimensions));
+ }
+
+ public function getSlicesLabel()
+ {
+ $parts = array();
+
+ $slices = $this->getSlices();
+ if (empty($slices)) {
+ return null;
+ }
+ foreach ($slices as $key => $value) {
+ $parts[] = sprintf('%s = %s', $this->getDimension($key)->getLabel(), $value);
+ }
+
+ return implode(', ', $parts);
+ }
+
+ /**
+ * Create a new dimension
+ *
+ * @param string $name
+ * @return Dimension
+ */
+ abstract public function createDimension($name);
+
+ protected function registerAvailableDimensions()
+ {
+ if ($this->availableDimensions !== null) {
+ return;
+ }
+
+ $this->availableDimensions = [];
+ foreach ($this->listAvailableDimensions() as $label) {
+ $name = strtolower($label);
+ if (! isset($this->availableDimensions[$name])) {
+ $this->availableDimensions[$name] = $this->createDimension($name)->setLabel($label);
+ } else {
+ $this->availableDimensions[$name]->addLabel($label);
+ }
+ }
+ }
+
+ public function listAdditionalDimensions()
+ {
+ $this->registerAvailableDimensions();
+
+ $list = [];
+ foreach ($this->availableDimensions as $name => $dimension) {
+ if (! $this->hasDimension($name)) {
+ $list[$name] = $dimension->getLabel();
+ }
+ }
+
+ return $list;
+ }
+
+ abstract public function listAvailableDimensions();
+
+ public function getDimensionAfter($name)
+ {
+ $found = false;
+ $after = null;
+
+ foreach ($this->listDimensions() as $k => $d) {
+ if ($found) {
+ $after = $d;
+ break;
+ }
+
+ if ($k === $name) {
+ $found = true;
+ }
+ }
+
+ return $after;
+ }
+
+ public function listDimensionsUpTo($name)
+ {
+ $res = array();
+ foreach ($this->listDimensions() as $d => $_) {
+ $res[] = $d;
+ if ($d === $name) {
+ break;
+ }
+ }
+
+ return $res;
+ }
+
+ public function moveDimensionUp($name)
+ {
+ $last = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($v === $name) {
+ $found = $k;
+ break;
+ }
+
+ $last = $k;
+ }
+
+ if ($found !== null) {
+ $this->flipPositions($positions, $last, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ public function moveDimensionDown($name)
+ {
+ $next = $found = null;
+ $positions = array_keys($this->dimensions);
+
+ foreach ($positions as $k => $v) {
+ if ($found !== null) {
+ $next = $k;
+ break;
+ }
+
+ if ($v === $name) {
+ $found = $k;
+ }
+ }
+
+ if ($next !== null) {
+ $this->flipPositions($positions, $next, $found);
+ }
+
+ $this->reOrderDimensions($positions);
+ return $this;
+ }
+
+ protected function flipPositions(&$array, $pos1, $pos2)
+ {
+ list(
+ $array[$pos1],
+ $array[$pos2]
+ ) = array(
+ $array[$pos2],
+ $array[$pos1]
+ );
+ }
+
+ protected function reOrderDimensions($positions)
+ {
+ $dimensions = array();
+ foreach ($positions as $pos => $key) {
+ $dimensions[$key] = $this->dimensions[$key];
+ }
+
+ $this->dimensions = $dimensions;
+ }
+
+ public function addDimension(Dimension $dimension)
+ {
+ $name = $dimension->getName();
+ if (array_key_exists($name, $this->dimensions)) {
+ throw new IcingaException('Cannot add dimension "%s" twice', $name);
+ }
+
+ $this->dimensions[$name] = $dimension;
+ return $this;
+ }
+
+ public function slice($key, $value)
+ {
+ if ($this->hasDimension($key)) {
+ $this->slices[$key] = $value;
+ } else {
+ throw new IcingaException('Got no such dimension: "%s"', $key);
+ }
+
+ return $this;
+ }
+
+ public function hasDimension($name)
+ {
+ return array_key_exists($name, $this->dimensions);
+ }
+
+ public function hasSlice($name)
+ {
+ return array_key_exists($name, $this->slices);
+ }
+
+ public function listSlices()
+ {
+ return array_keys($this->slices);
+ }
+
+ public function getSlices()
+ {
+ return $this->slices;
+ }
+
+ public function hasFact($name)
+ {
+ return array_key_exists($name, $this->chosenFacts);
+ }
+
+ public function getDimension($name)
+ {
+ return $this->dimensions[$name];
+ }
+
+ /**
+ * Return a list of chosen facts
+ *
+ * @return array
+ */
+ public function listFacts()
+ {
+ return $this->chosenFacts;
+ }
+
+ /**
+ * Choose a list of facts
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ $this->chosenFacts = $facts;
+ return $this;
+ }
+
+ public function listDimensions()
+ {
+ return array_diff_key($this->dimensions, $this->slices);
+ }
+
+ public function listColumns()
+ {
+ return array_merge(array_keys($this->listDimensions()), $this->listFacts());
+ }
+
+ /**
+ * @param View $view
+ * @param CubeRenderer $renderer
+ * @return string
+ */
+ public function render(View $view, CubeRenderer $renderer = null)
+ {
+ if ($renderer === null) {
+ $renderer = $this->getRenderer();
+ }
+
+ return $renderer->render($view);
+ }
+}
diff --git a/library/Cube/CubeRenderer.php b/library/Cube/CubeRenderer.php
new file mode 100644
index 0000000..3f8c80d
--- /dev/null
+++ b/library/Cube/CubeRenderer.php
@@ -0,0 +1,406 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Web\View;
+
+/**
+ * CubeRenderer base class
+ *
+ * Every Cube Renderer must extend this class.
+ *
+ * TODO: Should we introduce DimensionRenderer, FactRenderer and SummaryHelper
+ * instead?
+ *
+ * @package Icinga\Module\Cube
+ */
+abstract class CubeRenderer
+{
+ /** @var View */
+ protected $view;
+
+ /** @var Cube */
+ protected $cube;
+
+ /** @var array Our dimensions */
+ protected $dimensions;
+
+ /** @var array Our dimensions in regular order */
+ protected $dimensionOrder;
+
+ /** @var array Our dimensions in reversed order as a quick lookup source */
+ protected $reversedDimensions;
+
+ /** @var array Level (deepness) for each dimension (0, 1, 2...) */
+ protected $dimensionLevels;
+
+ protected $facts;
+
+ /** @var object The row before the current one */
+ protected $lastRow;
+
+ /**
+ * Current summaries
+ *
+ * This is an object of objects, with dimension names being the keys and
+ * a facts row containing current (rollup) summaries for that dimension
+ * being it's value
+ *
+ * @var object
+ */
+ protected $summaries;
+
+ protected $started;
+
+ /**
+ * CubeRenderer constructor.
+ *
+ * @param Cube $cube
+ */
+ public function __construct(Cube $cube)
+ {
+ $this->cube = $cube;
+ }
+
+ /**
+ * Render the given facts
+ *
+ * @param $facts
+ * @return string
+ */
+ abstract public function renderFacts($facts);
+
+ /**
+ * Returns the base url for the details action
+ *
+ * @return string
+ */
+ abstract protected function getDetailsBaseUrl();
+
+ /**
+ * Initialize all we need
+ */
+ protected function initialize()
+ {
+ $this->started = false;
+ $this->initializeDimensions()
+ ->initializeFacts()
+ ->initializeLastRow()
+ ->initializeSummaries();
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeLastRow()
+ {
+ $object = (object) array();
+ foreach ($this->dimensions as $dimension) {
+ $object->{$dimension->getName()} = null;
+ }
+
+ $this->lastRow = $object;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeDimensions()
+ {
+ $this->dimensions = $this->cube->listDimensions();
+
+ $min = 3;
+ $cnt = count($this->dimensions);
+ if ($cnt < $min) {
+ $pos = 0;
+ $diff = $min - $cnt;
+ $this->dimensionOrder = [];
+ foreach ($this->dimensions as $name => $_) {
+ $this->dimensionOrder[$pos++ + $diff] = $name;
+ }
+ } else {
+ $this->dimensionOrder = array_keys($this->dimensions);
+ }
+
+ $this->reversedDimensions = array_reverse($this->dimensionOrder);
+ $this->dimensionLevels = array_flip($this->dimensionOrder);
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeFacts()
+ {
+ $this->facts = $this->cube->listFacts();
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ protected function initializeSummaries()
+ {
+ $this->summaries = (object) array();
+ return $this;
+ }
+
+ /**
+ * @param object $row
+ * @return bool
+ */
+ protected function startsDimension($row)
+ {
+ foreach ($this->dimensionOrder as $name) {
+ if ($row->$name === null) {
+ $this->summaries->$name = $this->extractFacts($row);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param $row
+ * @return object
+ */
+ protected function extractFacts($row)
+ {
+ $res = (object) array();
+
+ foreach ($this->facts as $fact) {
+ $res->$fact = $row->$fact;
+ }
+
+ return $res;
+ }
+
+ public function render(View $view)
+ {
+ $this->view = $view;
+ $this->initialize();
+ $htm = $this->beginContainer();
+ foreach ($this->cube->fetchAll() as $row) {
+ $htm .= $this->renderRow($row);
+ }
+
+ return $htm . $this->closeDimensions() . $this->endContainer();
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = '';
+ if ($dimension = $this->startsDimension($row)) {
+ return $htm;
+ }
+
+ $htm .= $this->closeDimensionsForRow($row);
+ $htm .= $this->beginDimensionsForRow($row);
+ $htm .= $this->renderFacts($row);
+ $this->lastRow = $row;
+ return $htm;
+ }
+
+ protected function beginDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->beginDimensionsUpFrom($name, $row);
+ }
+ }
+
+ return '';
+ }
+
+ protected function beginDimensionsUpFrom($dimension, $row)
+ {
+ $htm = '';
+ $found = false;
+
+ foreach ($this->dimensionOrder as $name) {
+ if ($name === $dimension) {
+ $found = true;
+ }
+
+ if ($found) {
+ $htm .= $this->beginDimension($name, $row);
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensionsForRow($row)
+ {
+ $last = $this->lastRow;
+ foreach ($this->dimensionOrder as $name) {
+ if ($last->$name !== $row->$name) {
+ return $this->closeDimensionsDownTo($name);
+ }
+ }
+
+ return '';
+ }
+
+ protected function closeDimensionsDownTo($name)
+ {
+ $htm = '';
+
+ foreach ($this->reversedDimensions as $dimension) {
+ $htm .= $this->closeDimension($dimension);
+
+ if ($name === $dimension) {
+ break;
+ }
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimensions()
+ {
+ $htm = '';
+ foreach ($this->reversedDimensions as $name) {
+ $htm .= $this->closeDimension($name);
+ }
+
+ return $htm;
+ }
+
+ protected function closeDimension($name)
+ {
+ if (! $this->started) {
+ return '';
+ }
+
+ $indent = $this->getIndent($name);
+ return $indent . ' </div>' . "\n" . $indent . "</div><!-- $name -->\n";
+ }
+
+ protected function getIndent($name)
+ {
+ return str_repeat(' ', $this->getLevel($name));
+ }
+
+ protected function beginDimension($name, $row)
+ {
+ $indent = $this->getIndent($name);
+ if (! $this->started) {
+ $this->started = true;
+ }
+ $view = $this->view;
+ $dimension = $this->cube->getDimension($name);
+
+ return
+ $indent . '<div class="'
+ . $this->getDimensionClassString($name, $row)
+ . '">' . "\n"
+ . $indent . ' <div class="header"><a href="'
+ . $this->getDetailsUrl($name, $row)
+ . '" title="' . $view->escape(sprintf('Show details for %s: %s', $dimension->getLabel(), $row->$name)) . '"'
+ . ' data-base-target="_next">'
+ . $this->renderDimensionLabel($name, $row)
+ . '</a><a class="icon-filter" href="'
+ . $this->getSliceUrl($name, $row)
+ . '" title="' . $view->escape('Slice this cube') . '"></a></div>' . "\n"
+ . $indent . ' <div class="body">' . "\n";
+ }
+
+ /**
+ * Render the label for a given dimension name
+ *
+ * To have some context available, also
+ *
+ * @param $name
+ * @param $row
+ * @return string
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $caption = $row->$name;
+ if (empty($caption)) {
+ $caption = '_';
+ }
+
+ return $this->view->escape($caption);
+ }
+
+ protected function getDetailsUrl($name, $row)
+ {
+ $cube = $this->cube;
+
+ $dimensions = array_merge(array_keys($cube->listDimensions()), $cube->listSlices());
+ $params = [
+ 'dimensions' => DimensionParams::update($dimensions)->getParams()
+ ];
+
+ foreach ($this->cube->listDimensionsUpTo($name) as $dimensionName) {
+ $params[$dimensionName] = $row->$dimensionName;
+ }
+
+ foreach ($this->cube->getSlices() as $key => $val) {
+ $params[$key] = $val;
+ }
+
+ return $this->view->url(
+ $this->getDetailsBaseUrl(),
+ $params
+ );
+ }
+
+ protected function getSliceUrl($name, $row)
+ {
+ return $this->view->url()
+ ->setParam($name, $row->$name);
+ }
+
+ protected function isOuterDimension($name)
+ {
+ return $this->reversedDimensions[0] !== $name;
+ }
+
+ protected function getDimensionClassString($name, $row)
+ {
+ return implode(' ', $this->getDimensionClasses($name, $row));
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ return array('cube-dimension' . $this->getLevel($name));
+ }
+
+ protected function getLevel($name)
+ {
+ return $this->dimensionLevels[$name];
+ }
+
+ /**
+ * @return string
+ */
+ protected function beginContainer()
+ {
+ return '<div class="cube">' . "\n";
+ }
+
+ /**
+ * @return string
+ */
+ protected function endContainer()
+ {
+ return '</div>' . "\n";
+ }
+
+ /**
+ * Well... just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->cube);
+ }
+}
diff --git a/library/Cube/CubeRenderer/HostStatusCubeRenderer.php b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
new file mode 100644
index 0000000..89322cb
--- /dev/null
+++ b/library/Cube/CubeRenderer/HostStatusCubeRenderer.php
@@ -0,0 +1,105 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Icinga\Module\Cube\CubeRenderer;
+
+class HostStatusCubeRenderer extends CubeRenderer
+{
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->hosts_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+
+ $sums = $row;
+ if ($sums->hosts_down > 0) {
+ $classes[] = 'critical';
+ if ((int) $sums->hosts_unhandled_down === 0) {
+ $classes[] = 'handled';
+ }
+ } elseif ($sums->hosts_unreachable > 0) {
+ $classes[] = 'unreachable';
+ if ((int) $sums->hosts_unhandled_unreachable === 0) {
+ $classes[] = 'handled';
+ }
+ } else {
+ $classes[] = 'ok';
+ }
+
+ return $classes;
+ }
+
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = array();
+
+ if ($facts->hosts_unhandled_down > 0) {
+ $parts['critical'] = $facts->hosts_unhandled_down;
+ }
+
+ if ($facts->hosts_down > 0 && $facts->hosts_down > $facts->hosts_unhandled_down) {
+ $parts['critical handled'] = $facts->hosts_down - $facts->hosts_unhandled_down;
+ }
+
+ if ($facts->hosts_unhandled_unreachable > 0) {
+ $parts['unreachable'] = $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_unreachable > 0 && $facts->hosts_unreachable > $facts->hosts_unhandled_unreachable) {
+ $parts['unreachable handled'] = $facts->hosts_unreachable - $facts->hosts_unhandled_unreachable;
+ }
+
+ if ($facts->hosts_cnt > $facts->hosts_down && $facts->hosts_cnt > $facts->hosts_unreachable) {
+ $parts['ok'] = $facts->hosts_cnt - $facts->hosts_down - $facts->hosts_unreachable;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/hosts/details';
+ }
+}
diff --git a/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
new file mode 100644
index 0000000..33caa84
--- /dev/null
+++ b/library/Cube/CubeRenderer/ServiceStatusCubeRenderer.php
@@ -0,0 +1,126 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\CubeRenderer;
+
+use Icinga\Module\Cube\CubeRenderer;
+
+class ServiceStatusCubeRenderer extends CubeRenderer
+{
+ public function renderFacts($facts)
+ {
+ $indent = str_repeat(' ', 3);
+ $parts = [];
+
+ if ($facts->services_unhandled_critical > 0) {
+ $parts['critical'] = $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_critical > 0 && $facts->services_critical > $facts->services_unhandled_critical) {
+ $parts['critical handled'] = $facts->services_critical - $facts->services_unhandled_critical;
+ }
+
+ if ($facts->services_unhandled_warning > 0) {
+ $parts['warning'] = $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_warning > 0 && $facts->services_warning > $facts->services_unhandled_warning) {
+ $parts['warning handled'] = $facts->services_warning - $facts->services_unhandled_warning;
+ }
+
+ if ($facts->services_unhandled_unknown > 0) {
+ $parts['unknown'] = $facts->services_unhandled_unknown;
+ }
+
+ if ($facts->services_unknown > 0 && $facts->services_unknown > $facts->services_unhandled_unknown) {
+ $parts['unknown handled'] = $facts->services_unknown - $facts->services_unhandled_unknown;
+ }
+
+ if (
+ $facts->services_cnt > $facts->services_critical && $facts->services_cnt > $facts->services_warning
+ && $facts->services_cnt > $facts->services_unknown
+ ) {
+ $parts['ok'] = $facts->services_cnt - $facts->services_critical - $facts->services_warning -
+ $facts->services_unknown;
+ }
+
+ $main = '';
+ $sub = '';
+ foreach ($parts as $class => $count) {
+ if ($main === '') {
+ $main = $this->makeBadgeHtml($class, $count);
+ } else {
+ $sub .= $this->makeBadgeHtml($class, $count);
+ }
+ }
+ if ($sub !== '') {
+ $sub = $indent
+ . '<span class="others">'
+ . "\n "
+ . $sub
+ . $indent
+ . "</span>\n";
+ }
+
+ return $main . $sub;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function renderDimensionLabel($name, $row)
+ {
+ $htm = parent::renderDimensionLabel($name, $row);
+
+ if (($next = $this->cube->getDimensionAfter($name)) && isset($this->summaries->{$next->getName()})) {
+ $htm .= ' <span class="sum">(' . $this->summaries->{$next->getName()}->services_cnt . ')</span>';
+ }
+
+ return $htm;
+ }
+
+ protected function getDimensionClasses($name, $row)
+ {
+ $classes = parent::getDimensionClasses($name, $row);
+
+ $sums = $row;
+ if ($sums->services_critical > 0) {
+ $classes[] = 'critical';
+ if ((int) $sums->services_unhandled_critical === 0) {
+ $classes[] = 'handled';
+ }
+ } elseif ($sums->services_warning > 0) {
+ $classes[] = 'warning';
+ if ((int) $sums->services_unhandled_warning === 0) {
+ $classes[] = 'handled';
+ }
+ } elseif ($sums->services_unknown > 0) {
+ $classes[] = 'unknown';
+ if ((int) $sums->services_unhandled_unknown === 0) {
+ $classes[] = 'handled';
+ }
+ } else {
+ $classes[] = 'ok';
+ }
+
+ return $classes;
+ }
+
+ protected function makeBadgeHtml($class, $count)
+ {
+ $indent = str_repeat(' ', 3);
+
+ return sprintf(
+ '%s<span class="%s">%s</span>',
+ $indent,
+ $class,
+ $count
+ ) . "\n";
+ }
+
+ protected function getDetailsBaseUrl()
+ {
+ return 'cube/services/details';
+ }
+}
diff --git a/library/Cube/Dimension.php b/library/Cube/Dimension.php
new file mode 100644
index 0000000..7519da4
--- /dev/null
+++ b/library/Cube/Dimension.php
@@ -0,0 +1,52 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+/**
+ * Dimension interface
+ *
+ * All available dimensions must implement this interface
+ *
+ * @package Icinga\Module\Cube
+ */
+interface Dimension
+{
+ /**
+ * The name of this dimension
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * The label of this dimension
+ *
+ * @return string
+ */
+ public function getLabel();
+
+ /**
+ * Column expression
+ *
+ * This is the expression used to fetch the related column. Usually an SQL
+ * snippet when a relational database is involved
+ *
+ * @param Cube $cube
+ *
+ * @return string
+ */
+ public function getColumnExpression(Cube $cube);
+
+ /**
+ * Add this dimension to a cube
+ *
+ * This allows your dimension to apply itself to the Cube. That way your
+ * dimension is able to join optional tables and more
+ *
+ * @param Cube $cube
+ * @return void
+ */
+ public function addToCube(Cube $cube);
+}
diff --git a/library/Cube/DimensionParams.php b/library/Cube/DimensionParams.php
new file mode 100644
index 0000000..02b7ea1
--- /dev/null
+++ b/library/Cube/DimensionParams.php
@@ -0,0 +1,87 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2020 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube;
+
+use Icinga\Web\Url;
+
+class DimensionParams
+{
+ /**
+ * @var array Raw dimensions
+ */
+ protected $dimensions = [];
+
+ /**
+ * @var string encoded dimensions separated by coma
+ */
+ protected $params;
+
+ // For the form: DimensionsParam::fromUrl($url)
+ public static function fromUrl(Url $url)
+ {
+ return static::fromString($url->getParam('dimensions'));
+ }
+
+ public static function fromArray(array $dimensions = [])
+ {
+ $self = new static();
+
+ $self->dimensions = array_filter($dimensions);
+
+ return $self;
+ }
+
+ // For the controller: DimensionsParam::fromArray($this->params->shift('dimensions'))
+ public static function fromString($dimensions)
+ {
+ return static::fromArray(explode(',', $dimensions));
+ }
+
+
+ /**
+ * @param $dimension
+ *
+ * @return $this
+ */
+ public function add($dimension)
+ {
+ if (! empty($dimension)) {
+ $this->dimensions[] = $dimension;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Overwrite dimensions
+ *
+ * @param $dimensions
+ *
+ * @return $this
+ */
+ public static function update($dimensions)
+ {
+ $self = new static();
+ $self->dimensions = $dimensions;
+
+ return $self;
+ }
+
+ /**
+ * @return string encoded dimensions separated by coma
+ */
+ public function getParams()
+ {
+ return implode(',', $this->dimensions);
+ }
+
+ /**
+ * @return array
+ */
+ public function getDimensions()
+ {
+ return $this->dimensions;
+ }
+}
diff --git a/library/Cube/Hook/ActionsHook.php b/library/Cube/Hook/ActionsHook.php
new file mode 100644
index 0000000..8ba8a7c
--- /dev/null
+++ b/library/Cube/Hook/ActionsHook.php
@@ -0,0 +1,99 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Web\ActionLink;
+use Icinga\Module\Cube\Web\ActionLinks;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class ActionsHook
+{
+ /** @var ActionLinks */
+ private $actionLinks;
+
+ /**
+ * Your implementation should extend this method
+ *
+ * Then use the addActionLink() method, eventually combined with the
+ * createUrl() helper like this:
+ *
+ * <code>
+ * $this->addActionLink(
+ * $this->makeUrl('mymodule/controller/action', array('some' => 'param')),
+ * 'A shown title',
+ * 'A longer description text, should fit into the available square field',
+ * 'icon-name'
+ * );
+ * </code>
+ *
+ * For a list of available icon names please enable the Icinga Web 2 'doc'
+ * module and go to "Documentation" -> "Developer - Style" -> "Icons"
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return void
+ */
+ abstract public function prepareActionLinks(Cube $cube, View $view);
+
+ /**
+ * Lazy access to an ActionLinks object
+ *
+ * @return ActionLinks
+ */
+ public function getActionLinks()
+ {
+ if ($this->actionLinks === null) {
+ $this->actionLinks = new ActionLinks();
+ }
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method instantiating an ActionLink object
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function addActionLink(Url $url, $title, $description, $icon)
+ {
+ $this->getActionLinks()->add(
+ new ActionLink($url, $title, $description, $icon)
+ );
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ public function makeUrl($path, $params = null)
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+}
diff --git a/library/Cube/Hook/IcingaDbActionsHook.php b/library/Cube/Hook/IcingaDbActionsHook.php
new file mode 100644
index 0000000..63c24fe
--- /dev/null
+++ b/library/Cube/Hook/IcingaDbActionsHook.php
@@ -0,0 +1,125 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Hook;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use ipl\Web\Url;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+/**
+ * ActionsHook
+ *
+ * Implement this hook in case your module wants to add links to the detail
+ * page shown for a slice.
+ *
+ * @package Icinga\Module\Cube\Hook
+ */
+abstract class IcingaDbActionsHook
+{
+ /** @var Link[] */
+ private $actionLinks = [];
+
+ /**
+ * Create additional action links for the given cube
+ *
+ * @param IcingaDbCube $cube
+ * @return void
+ */
+ abstract public function createActionLinks(IcingaDbCube $cube);
+
+ /**
+ * Return the action links for the cube
+ *
+ * @return Link[]
+ */
+ final protected function getActionLinks(): array
+ {
+ return $this->actionLinks;
+ }
+
+ /**
+ * Helper method to populate action links array
+ *
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ *
+ * @return $this
+ */
+ final protected function addActionLink(Url $url, string $title, string $description, string $icon): self
+ {
+ $linkContent = (new HtmlDocument());
+ $linkContent->addHtml(new Icon($icon));
+ $linkContent->addHtml(HtmlElement::create('span', ['class' => 'title'], $title));
+ $linkContent->addHtml(HtmlElement::create('p', null, $description));
+
+ $this->actionLinks[] = new Link($linkContent, $url);
+
+ return $this;
+ }
+
+ /**
+ * Helper method instantiating an Url object
+ *
+ * @param string $path
+ * @param array $params
+ * @return Url
+ */
+ final protected function makeUrl(string $path, array $params = null): Url
+ {
+ $url = Url::fromPath($path);
+ if ($params !== null) {
+ $url->getParams()->mergeValues($params);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Render all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param IcingaDbCube $cube
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube)
+ {
+ $html = new HtmlDocument();
+
+ /** @var IcingaDbActionsHook $hook */
+ foreach (Hook::all('Cube/IcingaDbActions') as $hook) {
+ try {
+ $hook->createActionLinks($cube);
+ } catch (Exception $e) {
+ $html->addHtml(HtmlElement::create('li', ['class' => 'error'], $e->getMessage()));
+ }
+
+ foreach ($hook->getActionLinks() as $link) {
+ $html->addHtml(HtmlElement::create('li', null, $link));
+ }
+ }
+
+ if ($html->isEmpty()) {
+ $html->addHtml(
+ HtmlElement::create(
+ 'li',
+ ['class' => 'error'],
+ t('No action links have been provided for this cube')
+ )
+ );
+ }
+
+ return $html->render();
+ }
+}
diff --git a/library/Cube/IcingaDb/CustomVariableDimension.php b/library/Cube/IcingaDb/CustomVariableDimension.php
new file mode 100644
index 0000000..2266395
--- /dev/null
+++ b/library/Cube/IcingaDb/CustomVariableDimension.php
@@ -0,0 +1,119 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use ipl\Sql\Expression;
+use ipl\Stdlib\Filter;
+
+class CustomVariableDimension implements Dimension
+{
+ protected $name;
+
+ protected $label;
+
+ protected $wantNull = false;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getLabel()
+ {
+ return $this->label ?: $this->getName();
+ }
+
+ /**
+ * Set the label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ /**
+ * Add a label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function addLabel($label)
+ {
+ if ($this->label === null) {
+ $this->setLabel($label);
+ } else {
+ $this->label .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaDbCube $cube
+ * @return Expression|string
+ */
+ public function getColumnExpression(Cube $cube)
+ {
+ $expression = $cube->getDb()->quoteIdentifier(['c_' . $this->getName(), 'flatvalue']);
+
+ if ($this->wantNull) {
+ return new Expression("COALESCE($expression, '-')");
+ }
+
+ return $expression;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ /** @var IcingaDbCube $cube */
+ $name = $this->getName();
+ $innerQuery = $cube->innerQuery();
+ $sourceTable = $innerQuery->getModel()->getTableName();
+
+ $subQuery = $innerQuery->createSubQuery(new CustomvarFlat(), $sourceTable . '.vars');
+ $subQuery->getSelectBase()->resetWhere(); // The link to the outer query is the ON condition
+ $subQuery->columns(['flatvalue', 'object_id' => $sourceTable . '.id']);
+ $subQuery->filter(Filter::like('flatname', $name));
+
+ // Values might not be unique (wildcard dimensions)
+ $subQuery->getSelectBase()->groupBy([
+ $subQuery->getResolver()->getAlias($subQuery->getModel()) . '.flatvalue',
+ 'object_id'
+ ]);
+
+ $subQueryAlias = $cube->getDb()->quoteIdentifier(['c_' . $name]);
+ $innerQuery->getSelectBase()->groupBy($subQueryAlias . '.flatvalue');
+ $innerQuery->getSelectBase()->join(
+ [$subQueryAlias => $subQuery->assembleSelect()],
+ [$subQueryAlias . '.object_id = ' . $innerQuery->getResolver()->getAlias($innerQuery->getModel()) . '.id']
+ );
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbCube.php b/library/Cube/IcingaDb/IcingaDbCube.php
new file mode 100644
index 0000000..3f38d9b
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbCube.php
@@ -0,0 +1,196 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use ipl\Orm\Query;
+use ipl\Sql\Adapter\Pgsql;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+
+abstract class IcingaDbCube extends Cube
+{
+ use Auth;
+ use Database;
+
+ /** @var Query The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ protected $objectsFilter;
+
+ abstract public function getObjectsFilter();
+ /**
+ * An IcingaDbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return Query
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return Query
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = $this->prepareInnerQuery();
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * Get our rollup query
+ *
+ * @return Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ $this->addDimension($this->createDimension($name));
+
+ return $this;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ protected function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery()->getSelectBase();
+ $columns = [];
+ foreach ($this->dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+ $dimension->addToCube($this);
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+
+ if ($this->hasSlice($name)) {
+ $query->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$quotedDimension] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $query->columns($columns);
+ }
+
+ protected function prepareRollupQuery()
+ {
+ $dimensions = $this->listDimensions();
+ $this->finalizeInnerQuery();
+
+ $columns = [];
+ $groupBy = [];
+ foreach ($dimensions as $name => $dimension) {
+ $quotedDimension = $this->getDb()->quoteIdentifier([$name]);
+
+ $columns[$quotedDimension] = 'f.' . $quotedDimension;
+ $groupBy[] = $quotedDimension;
+ }
+
+ $availableFacts = $this->getAvailableFactColumns();
+
+ foreach ($this->chosenFacts as $alias) {
+ $columns[$alias] = new Expression('SUM(f.' . $availableFacts[$alias] . ')');
+ }
+
+ if (! empty($groupBy)) {
+ if ($this->getDb()->getAdapter() instanceof Pgsql) {
+ $groupBy = 'ROLLUP(' . implode(', ', $groupBy) . ')';
+ } else {
+ $groupBy[count($groupBy) - 1] .= ' WITH ROLLUP';
+ }
+ }
+
+ $rollupQuery = new Select();
+ $rollupQuery->from(['f' => $this->innerQuery()->assembleSelect()])
+ ->columns($columns)
+ ->groupBy($groupBy);
+
+ return $rollupQuery;
+ }
+
+ protected function prepareFullQuery()
+ {
+ $rollupQuery = $this->rollupQuery();
+ $columns = [];
+ foreach ($this->listColumns() as $column) {
+ $quotedColumn = $this->getDb()->quoteIdentifier([$column]);
+ $columns[$quotedColumn] = 'rollup.' . $this->getDb()->quoteIdentifier([$column]);
+ }
+
+ $fullQuery = new Select();
+ $fullQuery->from(['rollup' => $rollupQuery])->columns($columns);
+
+ foreach ($columns as $quotedColumn => $_) {
+ $fullQuery->orderBy("($quotedColumn IS NOT NULL)");
+ $fullQuery->orderBy($quotedColumn);
+ }
+
+ return $fullQuery;
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->getDb()->fetchAll($query);
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbHostStatusCube.php b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
new file mode 100644
index 0000000..028d4d7
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbHostStatusCube.php
@@ -0,0 +1,100 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\HoststateSummary;
+use ipl\Stdlib\Filter;
+
+class IcingaDbHostStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'hosts_cnt' => 'hosts_total',
+ 'hosts_down' => 'hosts_down_handled + f.hosts_down_unhandled',
+ 'hosts_unhandled_down' => 'hosts_down_unhandled',
+ 'hosts_unreachable' => 'hosts_unreachable',
+ 'hosts_unhandled_unreachable' => 'hosts_unreachable_unhandled'
+ ];
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function listAvailableDimensions()
+ {
+ $db = $this->getDb();
+
+ $query = CustomvarFlat::on($db);
+ $this->applyRestrictions($query);
+
+ $query
+ ->columns('flatname')
+ ->orderBy('flatname')
+ ->filter(Filter::like('host.id', '*'));
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->flatname);
+ $dimensions[$name] = $name;
+ }
+
+ return $dimensions;
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = HoststateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Host())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ $this->innerQuery = $query;
+ return $this->innerQuery;
+ }
+
+ /**
+ * Return Filter for Hosts cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $hosts = $this->innerQuery()->columns(['host' => 'host.name']);
+ $hosts->getSelectBase()->resetGroupBy();
+
+ $filter = Filter::any();
+
+ foreach ($hosts as $object) {
+ $filter->add(Filter::equal('host.name', $object->host));
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
new file mode 100644
index 0000000..9336cdf
--- /dev/null
+++ b/library/Cube/IcingaDb/IcingaDbServiceStatusCube.php
@@ -0,0 +1,109 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\IcingaDb;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Model\ServicestateSummary;
+use ipl\Stdlib\Filter;
+
+class IcingaDbServiceStatusCube extends IcingaDbCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVariableDimension($name);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'services_total',
+ 'services_critical' => 'services_critical_handled + f.services_critical_unhandled',
+ 'services_unhandled_critical' => 'services_critical_unhandled',
+ 'services_warning' => 'services_warning_handled + f.services_warning_unhandled',
+ 'services_unhandled_warning' => 'services_warning_unhandled',
+ 'services_unknown' => 'services_unknown_handled + f.services_unknown_unhandled',
+ 'services_unhandled_unknown' => 'services_unknown_unhandled',
+ ];
+ }
+
+ public function listAvailableDimensions()
+ {
+ $db = $this->getDb();
+
+ $query = CustomvarFlat::on($db);
+ $this->applyRestrictions($query);
+
+ $query
+ ->columns('flatname')
+ ->orderBy('flatname')
+ ->filter(Filter::like('service.id', '*'));
+ $query->getSelectBase()->groupBy('flatname');
+
+ $dimensions = [];
+ foreach ($query as $row) {
+ // Replaces array index notations with [*] to get results for arbitrary indexes
+ $name = preg_replace('/\\[\d+](?=\\.|$)/', '[*]', $row->flatname);
+ $dimensions[$name] = $name;
+ }
+
+ return $dimensions;
+ }
+
+ public function prepareInnerQuery()
+ {
+ $query = ServicestateSummary::on($this->getDb());
+ $query->columns(array_diff_key($query->getModel()->getColumns(), (new Service())->getColumns()));
+ $query->disableDefaultSort();
+ $this->applyRestrictions($query);
+
+ return $query;
+ }
+
+ /**
+ * Return Filter for Services cube.
+ *
+ * @return Filter\Any|Filter\Chain
+ */
+ public function getObjectsFilter()
+ {
+ if ($this->objectsFilter === null) {
+ $this->finalizeInnerQuery();
+
+ $services = $this->innerQuery()->columns([
+ 'host_name' => 'host.name',
+ 'service_name' => 'service.name'
+ ]);
+
+ $services->getSelectBase()->resetGroupBy();
+ $filter = Filter::any();
+
+ foreach ($services as $service) {
+ $filter->add(
+ Filter::all(
+ Filter::equal('service.name', $service->service_name),
+ Filter::equal('host.name', $service->host_name)
+ )
+ );
+ }
+
+ $this->objectsFilter = $filter;
+ }
+
+ return $this->objectsFilter;
+ }
+}
diff --git a/library/Cube/Ido/CustomVarDimension.php b/library/Cube/Ido/CustomVarDimension.php
new file mode 100644
index 0000000..0c21282
--- /dev/null
+++ b/library/Cube/Ido/CustomVarDimension.php
@@ -0,0 +1,158 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Dimension;
+
+/**
+ * CustomVarDimension
+ *
+ * This provides dimenstions for custom variables available in the IDO
+ *
+ * TODO: create safe aliases for special characters
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+class CustomVarDimension implements Dimension
+{
+ public const TYPE_HOST = 'host';
+
+ public const TYPE_SERVICE = 'service';
+
+ /**
+ * @var string custom variable name
+ */
+ protected $varName;
+
+ /**
+ * @var string custom variable label
+ */
+ protected $varLabel;
+
+ /**
+ * @var bool Whether null values should be shown
+ */
+ protected $wantNull = false;
+
+ /** @var string Type of the custom var */
+ protected $type;
+
+ /**
+ * CustomVarDimension constructor.
+ *
+ * @param $varName
+ * @param string $type Type of the custom var
+ */
+ public function __construct($varName, $type = null)
+ {
+ $this->varName = $varName;
+ $this->type = $type;
+ }
+
+ /**
+ * Define whether null values should be shown
+ *
+ * @param bool $wantNull
+ * @return $this
+ */
+ public function wantNull($wantNull = true)
+ {
+ $this->wantNull = $wantNull;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->varName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->varLabel ?: $this->getName();
+ }
+
+ /**
+ * Set the label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function setLabel($label)
+ {
+ $this->varLabel = $label;
+
+ return $this;
+ }
+
+ /**
+ * Add a label
+ *
+ * @param string $label
+ * @return $this
+ */
+ public function addLabel($label)
+ {
+ if ($this->varLabel === null) {
+ $this->setLabel($label);
+ } else {
+ $this->varLabel .= ' & ' . $label;
+ }
+
+ return $this;
+ }
+
+ public function getColumnExpression(Cube $cube)
+ {
+ /** @var IdoCube $cube */
+ if ($this->wantNull) {
+ return 'COALESCE(' . $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']) . ", '-')";
+ } else {
+ return $cube->db()->quoteIdentifier(['c_' . $this->varName, 'varvalue']);
+ }
+ }
+
+ protected function safeVarname($name)
+ {
+ return $name;
+ }
+
+ public function addToCube(Cube $cube)
+ {
+ switch ($this->type) {
+ case self::TYPE_HOST:
+ $objectId = 'ho.object_id';
+ break;
+ case self::TYPE_SERVICE:
+ $objectId = 'so.object_id';
+ break;
+ default:
+ $objectId = 'o.object_id';
+ }
+ $name = $this->safeVarname($this->varName);
+ /** @var $cube IdoCube */
+ $alias = $cube->db()->quoteIdentifier(['c_' . $name]);
+
+ if ($cube->isPgsql()) {
+ $on = "LOWER($alias.varname) = ?";
+ $name = strtolower($name);
+ } else {
+ $on = $alias . '.varname = ? COLLATE latin1_general_ci';
+ }
+
+ $cube->innerQuery()->joinLeft(
+ array($alias => $cube->tableName('icinga_customvariablestatus')),
+ $cube->db()->quoteInto($on, $name)
+ . ' AND ' . $alias . '.object_id = ' . $objectId,
+ array()
+ )->group($alias . '.varvalue');
+ }
+}
diff --git a/library/Cube/Ido/DataView/Hoststatus.php b/library/Cube/Ido/DataView/Hoststatus.php
new file mode 100644
index 0000000..32ee44b
--- /dev/null
+++ b/library/Cube/Ido/DataView/Hoststatus.php
@@ -0,0 +1,17 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\DataView;
+
+use Icinga\Data\ConnectionInterface;
+use Icinga\Module\Cube\Ido\Query\HoststatusQuery;
+
+class Hoststatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
+{
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ $this->connection = $connection;
+ $this->query = new HoststatusQuery($connection->getResource(), $columns);
+ }
+}
diff --git a/library/Cube/Ido/DbCube.php b/library/Cube/Ido/DbCube.php
new file mode 100644
index 0000000..5fc5b47
--- /dev/null
+++ b/library/Cube/Ido/DbCube.php
@@ -0,0 +1,298 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Data\Db\DbConnection;
+use Icinga\Module\Cube\Cube;
+
+abstract class DbCube extends Cube
+{
+ /** @var DbConnection */
+ protected $connection;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var ZfSelectWrapper The inner query fetching all required data */
+ protected $innerQuery;
+
+ /** @var \Zend_Db_Select The rollup query, creating grouped sums over innerQuery */
+ protected $rollupQuery;
+
+ /** @var \Zend_Db_Select The outer query, orders respecting NULL values, rollup first */
+ protected $fullQuery;
+
+ /** @var string Database name. Allows to eventually join over multiple dbs */
+ protected $dbName;
+
+ /** @var array Key/value array containing our chosen facts and the corresponding SQL expression */
+ protected $factColumns = array();
+
+ /**
+ * A DbCube must provide a list of all available columns
+ *
+ * This is a key/value array with the key being the fact name / column alias
+ * and
+ *
+ * @return array
+ */
+ abstract public function getAvailableFactColumns();
+
+ /**
+ * @return \Zend_Db_Select
+ */
+ abstract public function prepareInnerQuery();
+
+ /**
+ * Set a database connection
+ *
+ * @param DbConnection $connection
+ * @return $this
+ */
+ public function setConnection(DbConnection $connection)
+ {
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ return $this;
+ }
+
+ /**
+ * Prepare the query and fetch all data
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ $query = $this->fullQuery();
+ return $this->db()->fetchAll($query);
+ }
+
+ /**
+ * Choose a one or more facts
+ *
+ * This also initializes a fact column lookup array
+ *
+ * @param array $facts
+ * @return $this
+ */
+ public function chooseFacts(array $facts)
+ {
+ parent::chooseFacts($facts);
+
+ $this->factColumns = array();
+ $columns = $this->getAvailableFactColumns();
+ foreach ($this->chosenFacts as $name) {
+ $this->factColumns[$name] = $columns[$name];
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ * @return $this
+ */
+ public function setDbName($name)
+ {
+ $this->dbName = $name;
+ return $this;
+ }
+
+ /**
+ * Gives back the table name, eventually prefixed with a defined DB name
+ *
+ * @param string $name
+ * @return string
+ */
+ public function tableName($name)
+ {
+ if ($this->dbName === null) {
+ return $name;
+ } else {
+ return $this->dbName . '.' . $name;
+ }
+ }
+
+ /**
+ * Returns an eventually defined DB name
+ *
+ * @return string|null
+ */
+ public function getDbName()
+ {
+ return $this->dbName;
+ }
+
+ /**
+ * Get our inner query
+ *
+ * Hint: mostly used to get rid of NULL values
+ *
+ * @return ZfSelectWrapper
+ */
+ public function innerQuery()
+ {
+ if ($this->innerQuery === null) {
+ $this->innerQuery = new ZfSelectWrapper($this->prepareInnerQuery());
+ }
+
+ return $this->innerQuery;
+ }
+
+ /**
+ * We first prepare the queries and to finalize it later on
+ *
+ * This way dimensions can be added one by one, they will be allowed to
+ * optionally join additional tables or apply other modifications late
+ * in the process
+ *
+ * @return void
+ */
+ public function finalizeInnerQuery()
+ {
+ $query = $this->innerQuery()->unwrap();
+ $columns = array();
+ foreach ($this->dimensions as $name => $dimension) {
+ $dimension->addToCube($this);
+ if ($this->hasSlice($name)) {
+ $query->where(
+ $dimension->getColumnExpression($this) . ' = ?',
+ $this->slices[$name]
+ );
+ } else {
+ $columns[$name] = $dimension->getColumnExpression($this);
+ }
+ }
+
+ $c = [];
+
+ foreach ($columns + $this->factColumns as $k => $v) {
+ $c[$this->db()->quoteIdentifier([$k])] = $v;
+ }
+
+ $query->columns($c);
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function fullQuery()
+ {
+ if ($this->fullQuery === null) {
+ $this->fullQuery = $this->prepareFullQuery();
+ }
+
+ return $this->fullQuery;
+ }
+
+ /**
+ * Lazy-load our full query
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function rollupQuery()
+ {
+ if ($this->rollupQuery === null) {
+ $this->rollupQuery = $this->prepareRollupQuery();
+ }
+
+ return $this->rollupQuery;
+ }
+
+ /**
+ * The full query wraps the rollup query in a sub-query to work around
+ * MySQL limitations. This is required to not get into trouble when ordering,
+ * especially combined with the need to keep control over (eventually desired)
+ * NULL value fact columns
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareFullQuery()
+ {
+ $alias = 'rollup';
+ $cols = $this->listColumns();
+
+ $columns = array();
+
+ foreach ($cols as $col) {
+ $columns[$this->db()->quoteIdentifier([$col])] = $alias . '.' . $this->db()->quoteIdentifier([$col]);
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->rollupQuery()),
+ $columns
+ );
+
+ foreach ($columns as $col) {
+ $select->order('(' . $col . ' IS NOT NULL)');
+ $select->order($col);
+ }
+
+ return $select;
+ }
+
+ /**
+ * Provide access to our DB
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ return $this->db;
+ }
+
+ /**
+ * Whether our connection is PostgreSQL
+ *
+ * @return bool
+ */
+ public function isPgsql()
+ {
+ return $this->connection->getDbType() === 'pgsql';
+ }
+
+
+ /**
+ * This prepares the rollup query
+ *
+ * Inner query is wrapped in a subquery, summaries for all facts are
+ * fetched. Rollup considers all defined dimensions and expects them
+ * to exist as columns in the innerQuery
+ *
+ * @return \Zend_Db_Select
+ */
+ protected function prepareRollupQuery()
+ {
+ $alias = 'sub';
+
+ $dimensions = array_map(function ($val) {
+ return $this->db()->quoteIdentifier([$val]);
+ }, array_keys($this->listDimensions()));
+ $this->finalizeInnerQuery();
+ $columns = array();
+ foreach ($dimensions as $dimension) {
+ $columns[$dimension] = $alias . '.' . $dimension;
+ }
+
+ foreach ($this->listFacts() as $fact) {
+ $columns[$fact] = 'SUM(' . $fact . ')';
+ }
+
+ $select = $this->db()->select()->from(
+ array($alias => $this->innerQuery()->unwrap()),
+ $columns
+ );
+
+ if ($this->isPgsql()) {
+ $select->group('ROLLUP (' . implode(', ', $dimensions) . ')');
+ } else {
+ $select->group('(' . implode('), (', $dimensions) . ') WITH ROLLUP');
+ }
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoCube.php b/library/Cube/Ido/IdoCube.php
new file mode 100644
index 0000000..ca76b21
--- /dev/null
+++ b/library/Cube/Ido/IdoCube.php
@@ -0,0 +1,198 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Util\GlobFilter;
+
+/**
+ * IdoCube
+ *
+ * Base class for IDO-related cubes
+ *
+ * @package Icinga\Module\Cube\Ido
+ */
+abstract class IdoCube extends DbCube
+{
+ /** @var array */
+ protected $availableFacts = array();
+
+ /** @var string We ask for the IDO version for compatibility reasons */
+ protected $idoVersion;
+
+ /** @var MonitoringBackend */
+ protected $backend;
+
+ /**
+ * Cache for {@link filterProtectedCustomvars()}
+ *
+ * @var string|null
+ */
+ protected $protectedCustomvars;
+
+ /** @var GlobFilter The properties to hide from the user */
+ protected $blacklistedProperties;
+
+ /**
+ * We can steal the DB connection directly from a Monitoring backend
+ *
+ * @param MonitoringBackend $backend
+ * @return $this
+ */
+ public function setBackend(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+
+ $resource = $backend->getResource();
+ $resource->getDbAdapter()
+ ->getConnection()
+ ->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_NATURAL);
+
+ $this->setConnection($resource);
+
+ return $this;
+ }
+
+ /**
+ * Provice access to our DB resource
+ *
+ * This lazy-loads the default monitoring backend in case no DB has been
+ * given
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function db()
+ {
+ $this->requireBackend();
+ return parent::db();
+ }
+
+ /**
+ * Returns the Icinga IDO version
+ *
+ * @return string
+ */
+ protected function getIdoVersion()
+ {
+ if ($this->idoVersion === null) {
+ $db = $this->db();
+ $this->idoVersion = $db->fetchOne(
+ $db->select()->from('icinga_dbversion', 'version')
+ );
+ }
+
+ return $this->idoVersion;
+ }
+
+ /**
+ * Steal the default monitoring DB resource...
+ *
+ * ...in case none has been defined otherwise
+ *
+ * @return void
+ */
+ protected function requireBackend()
+ {
+ if ($this->db === null) {
+ $this->setBackend(MonitoringBackend::instance());
+ }
+ }
+
+ protected function getMonitoringRestriction()
+ {
+ $restriction = Filter::matchAny();
+ $restriction->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/i', $c);
+ }
+ ));
+
+ $filters = Auth::getInstance()->getUser()->getRestrictions('monitoring/filter/objects');
+
+ foreach ($filters as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAny();
+ }
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s',
+ 'monitoring/filter/objects',
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ return $restriction;
+ }
+
+ /**
+ * Return the given array without values matching the custom variables protected by the monitoring module
+ *
+ * @param string[] $customvars
+ *
+ * @return string[]
+ */
+ protected function filterProtectedCustomvars(array $customvars)
+ {
+ if ($this->blacklistedProperties === null) {
+ $this->blacklistedProperties = new GlobFilter(
+ Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
+ );
+ }
+
+ if ($this instanceof IdoServiceStatusCube) {
+ $type = 'service';
+ } else {
+ $type = 'host';
+ }
+
+ $customvars = $this->blacklistedProperties->removeMatching(
+ [$type => ['vars' => array_flip($customvars)]]
+ );
+
+ $customvars = isset($customvars[$type]['vars']) ? array_flip($customvars[$type]['vars']) : [];
+
+ if ($this->protectedCustomvars === null) {
+ $config = Config::module('monitoring')->get('security', 'protected_customvars');
+ $protectedCustomvars = array();
+
+ foreach (preg_split('~,~', $config, -1, PREG_SPLIT_NO_EMPTY) as $pattern) {
+ $regex = array();
+ foreach (explode('*', $pattern) as $literal) {
+ $regex[] = preg_quote($literal, '/');
+ }
+
+ $protectedCustomvars[] = implode('.*', $regex);
+ }
+
+ $this->protectedCustomvars = empty($protectedCustomvars)
+ ? '/^$/'
+ : '/^(?:' . implode('|', $protectedCustomvars) . ')$/';
+ }
+
+ return preg_grep($this->protectedCustomvars, $customvars, PREG_GREP_INVERT);
+ }
+}
diff --git a/library/Cube/Ido/IdoHostStatusCube.php b/library/Cube/Ido/IdoHostStatusCube.php
new file mode 100644
index 0000000..4881a9b
--- /dev/null
+++ b/library/Cube/Ido/IdoHostStatusCube.php
@@ -0,0 +1,112 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\HostStatusCubeRenderer;
+use Icinga\Module\Cube\Ido\DataView\Hoststatus;
+
+class IdoHostStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new HostStatusCubeRenderer($this);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAvailableFactColumns()
+ {
+ return array(
+ 'hosts_cnt' => 'SUM(CASE WHEN hs.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_down' => 'SUM(CASE WHEN hs.has_been_checked = 1 AND hs.current_state = 1'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN hs.current_state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unhandled_unreachable' => 'SUM(CASE WHEN hs.current_state = 2'
+ . ' AND hs.problem_has_been_acknowledged = 0 AND hs.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ );
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_HOST);
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * Right now this are just custom vars, we might support group memberships
+ * or other properties in future
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ if (count($this->filterProtectedCustomvars(array($name))) === 1) {
+ $this->addDimension($this->createDimension($name));
+ }
+
+ return $this;
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('hoststatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = ho.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ return $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = new Hoststatus($this->backend);
+
+ $view->getQuery()->requireColumn('host_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+}
diff --git a/library/Cube/Ido/IdoServiceStatusCube.php b/library/Cube/Ido/IdoServiceStatusCube.php
new file mode 100644
index 0000000..10a172a
--- /dev/null
+++ b/library/Cube/Ido/IdoServiceStatusCube.php
@@ -0,0 +1,113 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido;
+
+use Icinga\Module\Cube\CubeRenderer\ServiceStatusCubeRenderer;
+
+class IdoServiceStatusCube extends IdoCube
+{
+ public function getRenderer()
+ {
+ return new ServiceStatusCubeRenderer($this);
+ }
+
+ public function getAvailableFactColumns()
+ {
+ return [
+ 'services_cnt' => 'SUM(CASE WHEN ss.has_been_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unhandled_critical' => 'SUM(CASE WHEN ss.has_been_checked = 1 AND ss.current_state = 2'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN ss.current_state = 1 THEN 1 ELSE 0 END)',
+ 'services_unhandled_warning' => 'SUM(CASE WHEN ss.current_state = 1'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN ss.current_state = 3 THEN 1 ELSE 0 END)',
+ 'services_unhandled_unknown' => 'SUM(CASE WHEN ss.current_state = 3'
+ . ' AND ss.problem_has_been_acknowledged = 0 AND ss.scheduled_downtime_depth = 0'
+ . ' THEN 1 ELSE 0 END)',
+ ];
+ }
+
+ /**
+ * This returns a list of all available Dimensions
+ *
+ * @return array
+ */
+ public function listAvailableDimensions()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ $select
+ ->columns('cv.varname')
+ ->join(
+ ['cv' => $this->tableName('icinga_customvariablestatus')],
+ 'cv.object_id = so.object_id',
+ []
+ )
+ ->group('cv.varname');
+
+ if (version_compare($this->getIdoVersion(), '1.12.0', '>=')) {
+ $select->where('cv.is_json = 0');
+ }
+
+ $select->order('cv.varname');
+
+ return $this->filterProtectedCustomvars($this->db()->fetchCol($select));
+ }
+
+ public function prepareInnerQuery()
+ {
+ $this->requireBackend();
+
+ $view = $this->backend->select()->from('servicestatus');
+
+ $view->getQuery()->requireColumn('service_state');
+
+ $view->applyFilter($this->getMonitoringRestriction());
+
+ $select = $view->getQuery()->clearOrder()->getSelectQuery();
+
+ return $select;
+ }
+
+ /**
+ * Add a specific named dimension
+ *
+ * Right now this are just custom vars, we might support group memberships
+ * or other properties in future
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function addDimensionByName($name)
+ {
+ if (count($this->filterProtectedCustomvars([$name])) === 1) {
+ $this->addDimension($this->createDimension($name));
+ }
+
+ return $this;
+ }
+
+ public function createDimension($name)
+ {
+ $this->registerAvailableDimensions();
+
+ if (isset($this->availableDimensions[$name])) {
+ return clone $this->availableDimensions[$name];
+ }
+
+ return new CustomVarDimension($name, CustomVarDimension::TYPE_SERVICE);
+ }
+}
diff --git a/library/Cube/Ido/Query/HoststatusQuery.php b/library/Cube/Ido/Query/HoststatusQuery.php
new file mode 100644
index 0000000..6a9aa96
--- /dev/null
+++ b/library/Cube/Ido/Query/HoststatusQuery.php
@@ -0,0 +1,47 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Ido\Query;
+
+use Exception;
+use Icinga\Application\Version;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
+
+class HoststatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
+{
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup',
+ 'services' => 'servicestatus'
+ );
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'servicestatus') {
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+
+ protected function createSubQueryFilter(FilterExpression $filter, $queryName)
+ {
+ try {
+ return parent::createSubQueryFilter($filter, $queryName);
+ } catch (Exception $e) {
+ if (version_compare(Version::VERSION, '2.10.0', '>=')) {
+ throw $e;
+ }
+
+ if ($e->getMessage() === 'Undefined array key 0' && basename($e->getFile()) === 'IdoQuery.php') {
+ // Ensures compatibility with earlier Icinga Web 2 versions
+ throw new NotImplementedError('');
+ } else {
+ throw $e;
+ }
+ }
+ }
+}
diff --git a/library/Cube/Ido/ZfSelectWrapper.php b/library/Cube/Ido/ZfSelectWrapper.php
new file mode 100644
index 0000000..745d7a5
--- /dev/null
+++ b/library/Cube/Ido/ZfSelectWrapper.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Cube\Ido;
+
+/**
+ * Since version 1.1.0 we're using the monitoring module's queries as the cubes' base queries.
+ * Before, the host object table was available using the alias 'o'. Now it's 'ho'.
+ * Without this wrapper, the action link hook provided by the director would fail because it relies on the alias 'o'.
+ */
+class ZfSelectWrapper
+{
+ /** @var \Zend_Db_Select */
+ protected $select;
+
+ public function __construct(\Zend_Db_Select $select)
+ {
+ $this->select = $select;
+ }
+
+ /**
+ * Get the underlying Zend_Db_Select query
+ *
+ * @return \Zend_Db_Select
+ */
+ public function unwrap()
+ {
+ return $this->select;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::reset()}
+ */
+ public function reset($part = null)
+ {
+ $this->select->reset($part);
+
+ return $this;
+ }
+
+ /**
+ * {@see \Zend_Db_Select::columns()}
+ */
+ public function columns($cols = '*', $correlationName = null)
+ {
+ if (is_array($cols)) {
+ foreach ($cols as $alias => &$col) {
+ if (substr($col, 0, 2) === 'o.') {
+ $col = 'ho.' . substr($col, 2);
+ }
+ }
+ }
+
+ return $this->select->columns($cols, $correlationName);
+ }
+
+ /**
+ * Proxy Zend_Db_Select method calls
+ *
+ * @param string $name The name of the method to call
+ * @param array $arguments Arguments for the method to call
+ *
+ * @return mixed
+ *
+ * @throws \BadMethodCallException If the called method does not exist
+ */
+ public function __call($name, array $arguments)
+ {
+ if (! method_exists($this->select, $name)) {
+ $class = get_class($this);
+ $message = "Call to undefined method $class::$name";
+
+ throw new \BadMethodCallException($message);
+ }
+
+ return call_user_func_array([$this->select, $name], $arguments);
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/IcingaDbActions.php b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
new file mode 100644
index 0000000..c670ab4
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/IcingaDbActions.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\IcingaDb\IcingaDbCube;
+use Icinga\Module\Cube\Icingadb\IcingadbServiceStatusCube;
+
+class IcingaDbActions extends IcingaDbActionsHook
+{
+ public function createActionLinks(IcingaDbCube $cube)
+ {
+ $type = 'host';
+ if ($cube instanceof IcingadbServiceStatusCube) {
+ $type = 'service';
+ }
+
+ $url = 'icingadb/' . $type . 's';
+
+ $paramsWithPrefix = [];
+ foreach ($cube->getSlices() as $dimension => $slice) {
+ $paramsWithPrefix[$type . '.vars.' . $dimension] = $slice;
+ }
+
+ if ($type === 'host') {
+ $this->addActionLink(
+ $this->makeUrl($url, $paramsWithPrefix),
+ t('Show hosts status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'server'
+ );
+ } else {
+ $this->addActionLink(
+ $this->makeUrl($url, $paramsWithPrefix),
+ t('Show services status'),
+ t('This shows all matching hosts and their current state in Icinga DB Web'),
+ 'cog'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Cube/MonitoringActions.php b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
new file mode 100644
index 0000000..ae65c67
--- /dev/null
+++ b/library/Cube/ProvidedHook/Cube/MonitoringActions.php
@@ -0,0 +1,53 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Cube;
+
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Ido\IdoHostStatusCube;
+use Icinga\Module\Cube\Ido\IdoServiceStatusCube;
+use Icinga\Web\View;
+
+/**
+ * MonitoringActionLinks
+ *
+ * An action link hook implementation linking to matching hosts/services in the
+ * monitoring module
+ */
+class MonitoringActions extends ActionsHook
+{
+ public function prepareActionLinks(Cube $cube, View $view)
+ {
+ if ($cube instanceof IdoHostStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_host_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/hosts';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show hosts status'),
+ $view->translate('This shows all matching hosts and their current state in the monitoring module'),
+ 'host'
+ );
+ } elseif ($cube instanceof IdoServiceStatusCube) {
+ $vars = [];
+ foreach ($cube->getSlices() as $key => $val) {
+ $vars['_service_' . $key] = $val;
+ }
+
+ $url = 'monitoring/list/services';
+
+ $this->addActionLink(
+ $this->makeUrl($url, $vars),
+ $view->translate('Show services status'),
+ $view->translate('This shows all matching services and their current state in the monitoring module'),
+ 'host'
+ );
+ }
+ }
+}
diff --git a/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..a32310a
--- /dev/null
+++ b/library/Cube/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,11 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2022 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+}
diff --git a/library/Cube/Web/ActionLink.php b/library/Cube/Web/ActionLink.php
new file mode 100644
index 0000000..c9ad87b
--- /dev/null
+++ b/library/Cube/Web/ActionLink.php
@@ -0,0 +1,103 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionLinksHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLink
+{
+ /** @var Url */
+ protected $url;
+
+ /** @var string */
+ protected $title;
+
+ /** @var string */
+ protected $description;
+
+ /** @var string */
+ protected $icon;
+
+ /**
+ * ActionLink constructor.
+ * @param Url $url
+ * @param string $title
+ * @param string $description
+ * @param string $icon
+ */
+ public function __construct(Url $url, $title, $description, $icon)
+ {
+ $this->url = $url;
+ $this->title = $title;
+ $this->description = $description;
+ $this->icon = $icon;
+ }
+
+ /**
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Render our icon
+ *
+ * @param View $view
+ * @return string
+ */
+ protected function renderIcon(View $view)
+ {
+ return $view->icon($this->getIcon());
+ }
+
+ /**
+ * @param View $view
+ * @return string
+ */
+ public function render(View $view)
+ {
+ return sprintf(
+ '<a href="%s">%s<span class="title">%s</span><p>%s</p></a>',
+ $this->getUrl(),
+ $this->renderIcon($view),
+ $view->escape($this->getTitle()),
+ $view->escape($this->getDescription())
+ );
+ }
+}
diff --git a/library/Cube/Web/ActionLinks.php b/library/Cube/Web/ActionLinks.php
new file mode 100644
index 0000000..0c6cba4
--- /dev/null
+++ b/library/Cube/Web/ActionLinks.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Module\Cube\Cube;
+use Icinga\Module\Cube\Hook\ActionsHook;
+use Icinga\Web\View;
+
+/**
+ * ActionLink
+ *
+ * ActionsHook implementations return instances of this class
+ *
+ * @package Icinga\Module\Cube\Web
+ */
+class ActionLinks
+{
+ /** @var ActionLink[] */
+ protected $links = array();
+
+ /**
+ * Get all links for all Hook implementations
+ *
+ * This is what the Cube calls when rendering details
+ *
+ * @param Cube $cube
+ * @param View $view
+ *
+ * @return string
+ */
+ public static function renderAll(Cube $cube, View $view)
+ {
+ $html = array();
+
+ /** @var ActionsHook $hook */
+ foreach (Hook::all('Cube/Actions') as $hook) {
+ try {
+ $hook->prepareActionLinks($cube, $view);
+ } catch (Exception $e) {
+ $html[] = static::renderErrorItem($e, $view);
+ }
+
+ foreach ($hook->getActionLinks()->getLinks() as $link) {
+ $html[] = '<li>' . $link->render($view) . '</li>';
+ }
+ }
+
+ if (empty($html)) {
+ $html[] = static::renderErrorItem(
+ $view->translate('No action links have been provided for this cube'),
+ $view
+ );
+ }
+
+ return implode("\n", $html) . "\n";
+ }
+
+ /**
+ * @param Exception|string $error
+ * @param View $view
+ * @return string
+ */
+ private static function renderErrorItem($error, View $view)
+ {
+ if ($error instanceof Exception) {
+ $error = $error->getMessage();
+ }
+ return '<li class="error">' . $view->escape($error) . '</li>';
+ }
+
+ /**
+ * Add an ActionLink to this set of actions
+ *
+ * @param ActionLink $link
+ * @return $this
+ */
+ public function add(ActionLink $link)
+ {
+ $this->links[] = $link;
+ return $this;
+ }
+
+ /**
+ * @return ActionLink[]
+ */
+ public function getLinks()
+ {
+ return $this->links;
+ }
+
+ /**
+ * @param View $view
+ *
+ * @return string
+ */
+ public function render(View $view)
+ {
+ $links = $this->getLinks();
+ if (empty($links)) {
+ return '';
+ }
+
+ $html = '<ul class="action-links">';
+ foreach ($links as $link) {
+ $html .= $link->render($view);
+ }
+ $html .= '</ul>';
+
+ return $html;
+ }
+}
diff --git a/library/Cube/Web/Controller.php b/library/Cube/Web/Controller.php
new file mode 100644
index 0000000..b7405f9
--- /dev/null
+++ b/library/Cube/Web/Controller.php
@@ -0,0 +1,115 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Cube\DimensionParams;
+use Icinga\Module\Cube\Forms\DimensionsForm;
+use Icinga\Module\Cube\Hook\IcingaDbActionsHook;
+use Icinga\Module\Cube\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Web\Controller as WebController;
+use Icinga\Web\View;
+
+abstract class Controller extends WebController
+{
+ /** @var View This helps IDEs to understand that this is not ZF view */
+ public $view;
+
+ /** @var \Icinga\Module\Cube\Cube */
+ protected $cube;
+
+ /**
+ * Return this controllers' cube
+ *
+ * @return \Icinga\Module\Cube\Cube
+ */
+ abstract protected function getCube();
+
+ protected function moduleInit()
+ {
+ $this->cube = $this->getCube();
+ }
+
+ public function detailsAction()
+ {
+ $this->getTabs()->add('details', [
+ 'label' => $this->translate('Cube details'),
+ 'url' => $this->getRequest()->getUrl()
+ ])->activate('details');
+
+ $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns()));
+ $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions();
+ $wantNull = $this->params->shift('wantNull');
+
+ foreach ($vars as $var) {
+ $this->cube->addDimensionByName($var);
+ if ($wantNull) {
+ $this->cube->getDimension($var)->wantNull();
+ }
+ }
+
+ foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) {
+ $this->params->shift($p);
+ }
+
+ foreach ($this->params->toArray() as $param) {
+ $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1]));
+ }
+
+ $this->view->title = $this->cube->getSlicesLabel();
+ if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
+ $this->view->links = IcingaDbActionsHook::renderAll($this->cube);
+ } else {
+ $this->view->links = ActionLinks::renderAll($this->cube, $this->view);
+ }
+
+ $this->render('cube-details', null, true);
+ }
+
+ protected function renderCube()
+ {
+ // Hint: order matters, we are shifting!
+ $showSettings = $this->params->shift('showSettings');
+
+ $this->cube->chooseFacts(array_keys($this->cube->getAvailableFactColumns()));
+ $vars = DimensionParams::fromString($this->params->shift('dimensions', ''))->getDimensions();
+ $wantNull = $this->params->shift('wantNull');
+
+ foreach ($vars as $var) {
+ $this->cube->addDimensionByName($var);
+ if ($wantNull) {
+ $this->cube->getDimension($var)->wantNull();
+ }
+ }
+
+ foreach (['renderLayout', 'showFullscreen', 'showCompact', 'view'] as $p) {
+ $this->params->shift($p);
+ }
+
+ foreach ($this->params->toArray() as $param) {
+ $this->cube->slice(rawurldecode($param[0]), rawurldecode($param[1]));
+ }
+
+ $this->view->title = sprintf(
+ $this->translate('Cube: %s'),
+ $this->cube->getPathLabel()
+ );
+
+ if (count($this->cube->listDimensions()) > 0) {
+ $this->view->cube = $this->cube;
+ } else {
+ $showSettings = true;
+ }
+
+ if ($showSettings) {
+ $this->view->form = (new DimensionsForm())->setCube($this->cube);
+ $this->view->form->handleRequest();
+ } else {
+ $this->setAutorefreshInterval(15);
+ }
+
+ $this->render('cube-index', null, true);
+ }
+}
diff --git a/library/Cube/Web/IdoController.php b/library/Cube/Web/IdoController.php
new file mode 100644
index 0000000..2a024ae
--- /dev/null
+++ b/library/Cube/Web/IdoController.php
@@ -0,0 +1,24 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Cube\Web;
+
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+
+abstract class IdoController extends Controller
+{
+ public function createTabs()
+ {
+ return $this->getTabs()
+ ->add('cube/hosts', [
+ 'label' => $this->translate('Hosts'),
+ 'url' => 'cube/hosts' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString())
+ ])
+ ->add('cube/services', [
+ 'label' => $this->translate('Services'),
+ 'url' => 'cube/services' . ($this->params->toString() === '' ? '' : '?' . $this->params->toString())
+ ])
+ ->extend(new DashboardAction());
+ }
+}
diff --git a/module.info b/module.info
new file mode 100644
index 0000000..374e5b2
--- /dev/null
+++ b/module.info
@@ -0,0 +1,10 @@
+Name: Cube
+Version: 1.2.2
+Requires:
+ Libraries: icinga-php-library (>=0.9.0)
+ Modules: monitoring (>= 2.9.0), icingadb (>= 1.0.0)
+Description: Cube for Icinga Web
+ The Cube allows you to analyze data across multiple dimensions in your
+ Icinga Web frontend. Currently it shows host statistics (total count,
+ health) grouped by various custom variables in multiple dimensions. It
+ integrates well with other modules and provides extensible hooks
diff --git a/public/css/module.less b/public/css/module.less
new file mode 100644
index 0000000..150bb5a
--- /dev/null
+++ b/public/css/module.less
@@ -0,0 +1,343 @@
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+.controls > a {
+ color: @icinga-blue;
+
+ &:hover::before {
+ text-decoration: none;
+ }
+}
+
+div.cube {
+ div.cube-dimension0 > .header {
+ font-size: 1.2em;
+ font-weight: bold;
+ border-bottom: 1px solid @text-color;
+ }
+
+ div.cube-dimension1 {
+ clear: both;
+ border-left: 0.5em solid transparent;
+ }
+
+ div.cube-dimension1 > .header,
+ div.cube-dimension2 {
+ display: inline-block;
+ width: 10em;
+ height: 10em;
+ margin: 0;
+ padding: 0.2em;
+ margin-top: 0.3em;
+ overflow: hidden;
+ }
+
+ div.cube-dimension1 > .header {
+ display: flex;
+ float: left;
+ font-weight: bold;
+ text-align: center;
+ padding-left: 0;
+
+ > a:first-child {
+ flex: 1;
+ max-width: 8em;
+ word-wrap: break-word;
+ }
+ }
+
+ div.cube-dimension2 {
+ > .header {
+ display: flex;
+ text-align: center;
+ color: @text-color-on-icinga-blue;
+ margin-bottom: 1em;
+
+ > a:first-child {
+ flex: 1;
+ max-width: 8em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ background: @color-ok;
+
+ &.critical {
+ background: @color-critical;
+ }
+
+ &.warning {
+ background: @color-warning;
+ }
+
+ &.unknown {
+ background: @color-unknown;
+ }
+
+ &.unreachable {
+ background: @color-unreachable;
+ }
+
+ &.ok {
+ background: @color-ok;
+ }
+
+ > .body {
+ text-align: center;
+ }
+ }
+
+ span.critical, span.warning, span.unknown, span.unreachable, span.ok {
+ display: inline;
+ border-radius: 0.1em;
+ padding: 0.05em 0.2em;
+ color: @text-color-on-icinga-blue;
+ font-size: 3em;
+ }
+
+ span.ok {
+ background: @color-ok;
+ }
+
+ .others {
+ display: flex;
+ justify-content: center;
+ span {
+ margin-left: 0.2em;
+ margin-top: 0.8em;
+ font-size: 1em;
+ }
+ }
+
+ span.critical {
+ background: @color-critical;
+ }
+
+ span.critical.handled {
+ background: @color-critical-handled;
+ }
+
+ div.cube-dimension0.critical > .header {
+ color: @color-critical;
+ border-color: @color-critical;
+ }
+
+ div.cube-dimension1.critical {
+ border-left-color: @color-critical;
+
+ &.handled {
+ border-left-color: @color-critical-handled;
+ }
+ }
+
+ div.cube-dimension2.critical {
+ span.sum {
+ font-size: 1em;
+ }
+ }
+
+ span.warning {
+ background: @color-warning;
+ }
+
+ span.warning.handled {
+ background: @color-warning-handled;
+ }
+
+ div.cube-dimension0.warning > .header {
+ color: @color-warning;
+ border-color: @color-warning;
+ }
+
+ div.cube-dimension1.warning {
+ border-left-color: @color-warning;
+
+ &.handled {
+ border-left-color: @color-warning-handled;
+ }
+ }
+
+ div.cube-dimension2.warning {
+ span.sum {
+ font-size: 1em;
+ }
+ }
+
+ span.unknown{
+ background: @color-unknown;
+ }
+
+ span.unknown.handled {
+ background: @color-unknown-handled;
+ }
+
+ div.cube-dimension0.unknown > .header {
+ color: @color-unknown;
+ border-color: @color-unknown;
+ }
+
+ div.cube-dimension1.unknown {
+ border-left-color: @color-unknown;
+
+ &.handled {
+ border-left-color: @color-unknown-handled;
+ }
+ }
+
+ div.cube-dimension2.unknown {
+ span.sum {
+ font-size: 1em;
+ }
+ }
+}
+
+span.unreachable{
+ background: @color-unreachable;
+}
+
+span.unreachable.handled {
+ background: @color-unreachable-handled;
+}
+
+div.cube-dimension0.unreachable > .header {
+ color: @color-unreachable;
+ border-color: @color-unreachable;
+}
+
+div.cube-dimension1.unreachable {
+ border-left-color: @color-unreachable;
+
+ &.handled {
+ border-left-color: @color-unreachable-handled;
+ }
+}
+
+div.cube-dimension2.unreachable {
+ span.sum {
+ font-size: 1em;
+ }
+}
+
+fieldset.dimensions {
+ margin: 0;
+ padding: 0;
+
+ span {
+ min-width: 18em;
+ display: inline-block;
+ }
+
+ button[type=submit] {
+ border: none;
+ color: @text-color;
+ background: none;
+
+ padding-left: .75em;
+ padding-right: .75em;
+
+ i:before {
+ margin-right: 0;
+ }
+
+ &:not([disabled]):hover {
+ .rounded-corners();
+ background-color: @icinga-blue;
+ color: @text-color-on-icinga-blue;
+ }
+ }
+
+ button[type=submit][disabled] {
+ color: @disabled-gray;
+ background-color: @body-bg-color;
+ cursor: auto;
+ }
+
+ &:first-of-type button[type=submit]:nth-of-type(2) {
+ margin-left: 2.75em;
+ }
+
+ &:not(:first-of-type) {
+ button[type=submit]:nth-of-type(2):last-of-type {
+ margin-right: 2.75em;
+ }
+ }
+
+ &:last-of-type {
+ margin-bottom: 0.5em;
+ }
+}
+
+.dimension-name {
+ font-weight: bold;
+ margin-left: 1em;
+}
+
+ul.action-links {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+ul.action-links {
+ li {
+ margin: 0;
+ padding: 0;
+ float: left;
+ display: inline-block;
+ width: 18em;
+ height: 10em;
+ overflow: hidden;
+ }
+
+ li.error {
+ background-color: @color-critical;
+ color: @text-color-on-icinga-blue;
+ padding: 1em;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ a {
+ width: 100%;
+ height: 100%;
+ padding: 0.5em 1em;
+ display: block;
+ text-decoration: none;
+ color: @gray;
+
+ .title {
+ margin-left: 5em;
+ font-weight: bold;
+ color: @link-color;
+ }
+
+ p {
+ margin-left: 5em;
+ }
+
+ i {
+ position: absolute;
+ font-size: 3em;
+ display: inline-block;
+ }
+ &:hover {
+ background: @tr-hover-color;
+ text-decoration: none;
+ }
+ }
+}
+
+/** Form **/
+
+.content form {
+ margin-bottom: 2em;
+}
+
+form button[type=submit] {
+ margin-top: 0.5em;
+}
+
+select {
+ width: 100%;
+}
diff --git a/run.php b/run.php
new file mode 100644
index 0000000..afe0b43
--- /dev/null
+++ b/run.php
@@ -0,0 +1,8 @@
+<?php
+
+// Icinga Web 2 Cube Module | (c) 2016 Icinga GmbH | GPLv2
+
+$this->provideHook('cube/Actions', 'Cube/MonitoringActions');
+$this->provideHook('cube/IcingaDbActions', 'Cube/IcingaDbActions');
+
+$this->provideHook('icingadb/icingadbSupport');