summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md17
-rw-r--r--LICENSE339
-rw-r--r--README.md25
-rw-r--r--application/clicommands/Icinga2Command.php206
-rw-r--r--application/controllers/ConfigController.php30
-rw-r--r--application/controllers/GraphController.php172
-rw-r--r--application/controllers/HostsController.php112
-rw-r--r--application/controllers/ListController.php192
-rw-r--r--application/controllers/MonitoringGraphController.php155
-rw-r--r--application/controllers/ServicesController.php115
-rw-r--r--application/forms/Config/AdvancedForm.php95
-rw-r--r--application/forms/Config/BackendForm.php59
-rw-r--r--application/forms/TimeRangePicker/CommonForm.php196
-rw-r--r--application/forms/TimeRangePicker/CustomForm.php246
-rw-r--r--application/views/scripts/config/advanced.phtml7
-rw-r--r--application/views/scripts/config/backend.phtml7
-rw-r--r--application/views/scripts/list/hosts.phtml67
-rw-r--r--application/views/scripts/list/services.phtml77
-rw-r--r--application/views/scripts/test/apache.phtml12
-rw-r--r--application/views/scripts/test/cpu.phtml28
-rw-r--r--configuration.php31
-rw-r--r--doc/01-About.md19
-rw-r--r--doc/02-Installation.md48
-rw-r--r--doc/02-Installation.md.d/From-Source.md14
-rw-r--r--doc/03-Configuration.md65
-rw-r--r--doc/04-Templates.md226
-rw-r--r--doc/05-Troubleshooting.md85
-rw-r--r--doc/06-Development.md15
-rw-r--r--doc/img/service-detail-view.pngbin0 -> 96019 bytes
-rw-r--r--doc/img/service-list.pngbin0 -> 178751 bytes
-rw-r--r--library/Graphite/Graphing/Chart.php385
-rw-r--r--library/Graphite/Graphing/GraphingTrait.php79
-rw-r--r--library/Graphite/Graphing/GraphiteWebClient.php198
-rw-r--r--library/Graphite/Graphing/MetricsDataSource.php48
-rw-r--r--library/Graphite/Graphing/MetricsQuery.php219
-rw-r--r--library/Graphite/Graphing/Template.php364
-rw-r--r--library/Graphite/Graphing/Templates.php321
-rw-r--r--library/Graphite/ProvidedHook/Icingadb/HostDetailExtension.php46
-rw-r--r--library/Graphite/ProvidedHook/Icingadb/IcingadbSupport.php9
-rw-r--r--library/Graphite/ProvidedHook/Icingadb/ServiceDetailExtension.php46
-rw-r--r--library/Graphite/ProvidedHook/Monitoring/DetailviewExtension.php40
-rw-r--r--library/Graphite/Util/IcingadbUtils.php49
-rw-r--r--library/Graphite/Util/InternalProcessTracker.php126
-rw-r--r--library/Graphite/Util/MacroTemplate.php239
-rw-r--r--library/Graphite/Util/TimeRangePickerTools.php111
-rw-r--r--library/Graphite/Web/Controller/IcingadbGraphiteController.php110
-rw-r--r--library/Graphite/Web/Controller/MonitoringAwareController.php175
-rw-r--r--library/Graphite/Web/Controller/TimeRangePickerTrait.php115
-rw-r--r--library/Graphite/Web/FakeSchemeRequest.php18
-rw-r--r--library/Graphite/Web/Form/Decorator/Proxy.php47
-rw-r--r--library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php42
-rw-r--r--library/Graphite/Web/Form/Validator/HttpUserValidator.php30
-rw-r--r--library/Graphite/Web/Form/Validator/MacroTemplateValidator.php21
-rw-r--r--library/Graphite/Web/Widget/GraphImage.php143
-rw-r--r--library/Graphite/Web/Widget/Graphs.php688
-rw-r--r--library/Graphite/Web/Widget/Graphs/Host.php51
-rw-r--r--library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php61
-rw-r--r--library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php71
-rw-r--r--library/Graphite/Web/Widget/Graphs/Service.php56
-rw-r--r--library/Graphite/Web/Widget/IcingadbGraphs.php106
-rw-r--r--library/Graphite/Web/Widget/InlineGraphImage.php49
-rw-r--r--library/vendor/Psr/Http/Message/MessageInterface.php187
-rw-r--r--library/vendor/Psr/Http/Message/RequestInterface.php129
-rw-r--r--library/vendor/Psr/Http/Message/ResponseInterface.php68
-rw-r--r--library/vendor/Psr/Http/Message/ServerRequestInterface.php261
-rw-r--r--library/vendor/Psr/Http/Message/StreamInterface.php158
-rw-r--r--library/vendor/Psr/Http/Message/UploadedFileInterface.php123
-rw-r--r--library/vendor/Psr/Http/Message/UriInterface.php324
-rw-r--r--library/vendor/Psr/LICENSE19
-rw-r--r--library/vendor/Psr/Loader.php21
-rw-r--r--library/vendor/iplx/Http/Client.php199
-rw-r--r--library/vendor/iplx/Http/ClientInterface.php22
-rw-r--r--library/vendor/iplx/Http/Handle.php32
-rw-r--r--library/vendor/iplx/Http/MessageTrait.php174
-rw-r--r--library/vendor/iplx/Http/Request.php143
-rw-r--r--library/vendor/iplx/Http/Response.php64
-rw-r--r--library/vendor/iplx/Http/Stream.php283
-rw-r--r--library/vendor/iplx/Http/Uri.php202
-rw-r--r--library/vendor/iplx/LICENSE339
-rw-r--r--library/vendor/iplx/Loader.php20
-rw-r--r--library/vendor/iplx/README1
-rw-r--r--module.info8
-rw-r--r--public/css/module.less213
-rw-r--r--public/js/module.js112
-rw-r--r--run.php32
-rw-r--r--templates/cpu_windows_powershell_framework.ini19
-rw-r--r--templates/default.ini26
-rw-r--r--templates/disk.ini22
-rw-r--r--templates/disk_windows_powershell_framework.ini40
-rw-r--r--templates/ethMon.ini34
-rw-r--r--templates/file_age.ini32
-rw-r--r--templates/fping.ini32
-rw-r--r--templates/graphite-template memory-linux-local.ini28
-rw-r--r--templates/hostalive.ini32
-rw-r--r--templates/icinga.ini302
-rw-r--r--templates/icmp-hosts.ini38
-rw-r--r--templates/icmp.ini36
-rw-r--r--templates/interfacetable.ini52
-rw-r--r--templates/load.ini35
-rw-r--r--templates/mailq.ini15
-rw-r--r--templates/memory.ini17
-rw-r--r--templates/memory_windows_powershell_framework.ini42
-rw-r--r--templates/multi.ini54
-rw-r--r--templates/mysql.ini270
-rw-r--r--templates/mysql_health.ini456
-rw-r--r--templates/netapp_cdot_aggregates.ini22
-rw-r--r--templates/netapp_cdot_volumes.ini44
-rw-r--r--templates/network.ini35
-rw-r--r--templates/ntp.ini65
-rw-r--r--templates/nwc-interface-usage.ini58
-rw-r--r--templates/nwc-load.ini19
-rw-r--r--templates/ping.ini32
-rw-r--r--templates/procs.ini15
-rw-r--r--templates/response-size-hosts.ini16
-rw-r--r--templates/response-size.ini15
-rw-r--r--templates/response-time-hosts.ini16
-rw-r--r--templates/response-time.ini15
-rw-r--r--templates/snmp-int.ini54
-rw-r--r--templates/snmp-load-netsl.ini19
-rw-r--r--templates/snmp-load-stand.ini20
-rw-r--r--templates/snmp-memory.ini23
-rw-r--r--templates/snmp-storage.ini21
-rw-r--r--templates/swap.ini17
-rw-r--r--templates/update.ini34
-rw-r--r--templates/uptime.ini32
-rw-r--r--templates/users.ini15
126 files changed, 12026 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..710cc82
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Graphite module for Icinga Web 2 - changelog
+
+## Version 1.1.0
+
+This release fixes a few bugs in monitored object attributes considering,
+special character handling, PHP 7 compatibility and UI performance.
+It also adds a [graph assembly debugger](doc/05-Troubleshooting.md).
+
+## Version 1.0.1
+
+This release just fixes a mismatch between the Git version tag and the version
+in module.info.
+
+## Version 1.0.0
+
+First stable release. You can find its features on
+[the Icinga blog](https://www.icinga.com/2018/03/21/graphite-module-for-icinga-web-2-released/).
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..54a1206
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+# Icinga Web Graphite Integration
+
+[![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-graphite/workflows/PHP%20Tests/badge.svg?branch=master)
+[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-graphite.svg)](https://github.com/Icinga/icingaweb2-module-graphite)
+
+![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png)
+
+This module integrates an existing [Graphite](https://graphite.readthedocs.io/en/latest/)
+installation in your [Icinga Web 2](https://icinga.com/products/infrastructure-monitoring/)
+frontend.
+
+![Service List](doc/img/service-list.png)
+![Detail View](doc/img/service-detail-view.png)
+
+It provides a new menu section with two general overviews for hosts and
+services as well as an extension to the host and service detail view of
+the monitoring module.
+
+## Documentation
+
+* [Installation](https://icinga.com/docs/icinga-web-graphite-integration/latest/doc/02-Installation/)
+* [Configuration](https://icinga.com/docs/icinga-web-graphite-integration/latest/doc/03-Configuration/)
+* [Templates](https://icinga.com/docs/icinga-web-graphite-integration/latest/doc/04-Templates/)
+* [Development](https://icinga.com/docs/icinga-web-graphite-integration/latest/doc/06-Development/)
diff --git a/application/clicommands/Icinga2Command.php b/application/clicommands/Icinga2Command.php
new file mode 100644
index 0000000..816e063
--- /dev/null
+++ b/application/clicommands/Icinga2Command.php
@@ -0,0 +1,206 @@
+<?php
+
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Graphite\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+
+class Icinga2Command extends Command
+{
+ use GraphingTrait;
+
+ /**
+ * Generate Icinga 2 host and service config based on the present graph templates
+ *
+ * The generated (fictive) monitored objects' checks yield random perfdata to be
+ * written to Graphite as expected by the present graph templates of this module.
+ * The generated Icinga 2 config can be used to simulate graphs generated based
+ * on the graph templates.
+ *
+ * icingacli graphite icinga2 config
+ */
+ public function configAction()
+ {
+ $icinga2CfgObjPrefix = 'IW2_graphite_demo';
+ $obscuredCheckCommandCustomVar = Graphs::getObscuredCheckCommandCustomVar();
+
+ $result = [
+ <<<EOT
+object CheckCommand "$icinga2CfgObjPrefix" {
+ command = [ "/usr/bin/printf" ]
+ arguments = {
+ "%s" = {{
+ var res = " |"
+ for (label => max in macro("\$$icinga2CfgObjPrefix\$")) {
+ res += " '" + label + "'=" + (random() % max) + ";" + (max * 0.8) + ";" + (max * 0.9) + ";0;" + max
+ }
+ res
+ }}
+ }
+}
+EOT
+ ,
+ <<<EOT
+object HostGroup "$icinga2CfgObjPrefix" {
+ assign where host.vars.$icinga2CfgObjPrefix
+}
+EOT
+ ,
+ <<<EOT
+object ServiceGroup "$icinga2CfgObjPrefix" {
+ assign where service.vars.$icinga2CfgObjPrefix
+}
+EOT
+ ,
+ <<<EOT
+object Host "{$icinga2CfgObjPrefix}_doesntmatchanycheckcommand" {
+ check_command = "$icinga2CfgObjPrefix"
+ check_interval = 30s
+ vars.$obscuredCheckCommandCustomVar = "doesntmatchanycheckcommand"
+ vars.$icinga2CfgObjPrefix = {
+ "dummy1" = 100
+ "dummy2" = 100
+ "dummy3" = 100
+ "dummy4" = 100
+ }
+}
+EOT
+ ,
+ <<<EOT
+apply Service "{$icinga2CfgObjPrefix}_doesntmatchanycheckcommand" {
+ assign where host.vars.$icinga2CfgObjPrefix
+ check_command = "$icinga2CfgObjPrefix"
+ check_interval = 30s
+ vars.$obscuredCheckCommandCustomVar = "doesntmatchanycheckcommand"
+ vars.$icinga2CfgObjPrefix = {
+ "dummy1" = 100
+ "dummy2" = 100
+ "dummy3" = 100
+ "dummy4" = 100
+ }
+}
+EOT
+ ];
+
+ foreach (static::getAllTemplates()->getAllTemplates() as $checkCommand => $templates) {
+ $perfdata = [];
+
+ foreach ($templates as $templateName => $template) {
+ /** @var Template $template */
+
+ $urlParams = $template->getUrlParams();
+
+ switch (isset($urlParams['yUnitSystem']) ? $urlParams['yUnitSystem']->resolve([]) : 'none') {
+ case 'si':
+ case 'binary':
+ $max = 42000000;
+ break;
+
+ case 'sec':
+ case 'msec':
+ $max = 82800;
+ break;
+
+ default:
+ $max = 100;
+ }
+
+ foreach ($template->getCurves() as $curveName => $curve) {
+ /** @var MacroTemplate $metricFilter */
+ $metricFilter = $curve[0];
+
+ $macros = array_flip($metricFilter->getMacros());
+ $service = isset($macros['service_name_template']);
+
+ foreach ($macros as & $macro) {
+ $macro = ['dummy1', 'dummy2', 'dummy3', 'dummy4'];
+ }
+
+ $macros['host_name_template'] = [''];
+ $macros['service_name_template'] = [''];
+
+ foreach ($this->cartesianProduct($macros) as $macroValues) {
+ if (
+ preg_match(
+ '/\A\.[^.]+\.(.+)\.[^.]+\z/',
+ $metricFilter->resolve($macroValues),
+ $match
+ )
+ ) {
+ $perfdata[$match[1]] = $max;
+ }
+ }
+ }
+ }
+
+ $monObj = $service
+ ? [
+ "apply Service \"{$icinga2CfgObjPrefix}_{$checkCommand}\" {",
+ " assign where host.vars.$icinga2CfgObjPrefix"
+ ]
+ : ["object Host \"{$icinga2CfgObjPrefix}_{$checkCommand}\" {"];
+
+ $monObj[] = " check_command = \"$icinga2CfgObjPrefix\"";
+ $monObj[] = ' check_interval = 30s';
+ $monObj[] = " vars.$obscuredCheckCommandCustomVar = \"$checkCommand\"";
+ $monObj[] = " vars.$icinga2CfgObjPrefix = {";
+
+ foreach ($perfdata as $label => $max) {
+ $monObj[] = " \"$label\" = $max";
+ }
+
+ $monObj[] = ' }';
+ $monObj[] = '}';
+
+ $result[] = implode("\n", $monObj);
+ }
+
+ echo implode("\n\n", $result) . "\n";
+ }
+
+ /**
+ * Generate the cartesian product of the given array
+ *
+ * [
+ * 'a' => ['b', 'c'],
+ * 'd' => ['e', 'f']
+ * ]
+ *
+ * [
+ * ['a' => 'b', 'd' => 'e'],
+ * ['a' => 'b', 'd' => 'f'],
+ * ['a' => 'c', 'd' => 'e'],
+ * ['a' => 'c', 'd' => 'f']
+ * ]
+ *
+ * @param array[] $input
+ *
+ * @return array[]
+ */
+ protected function cartesianProduct(array &$input)
+ {
+ $results = [[]];
+
+ foreach ($input as $key => & $values) {
+ $nextStep = [];
+
+ foreach ($results as & $result) {
+ foreach ($values as $value) {
+ $nextStep[] = array_merge($result, [$key => $value]);
+ }
+ }
+ unset($result);
+
+ $results = & $nextStep;
+ unset($nextStep);
+ }
+ unset($values);
+
+ return $results;
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..f627e36
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Module\Graphite\Forms\Config\AdvancedForm;
+use Icinga\Module\Graphite\Forms\Config\BackendForm;
+use Icinga\Web\Controller;
+
+class ConfigController extends Controller
+{
+ public function init()
+ {
+ $this->assertPermission('config/modules');
+ parent::init();
+ }
+
+ public function backendAction()
+ {
+ $this->view->form = $form = new BackendForm();
+ $form->setIniConfig($this->Config())->handleRequest();
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('backend');
+ }
+
+ public function advancedAction()
+ {
+ $this->view->form = $form = new AdvancedForm();
+ $form->setIniConfig($this->Config())->handleRequest();
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('advanced');
+ }
+}
diff --git a/application/controllers/GraphController.php b/application/controllers/GraphController.php
new file mode 100644
index 0000000..c8dc7db
--- /dev/null
+++ b/application/controllers/GraphController.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Util\IcingadbUtils;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Web\Controller;
+use Icinga\Web\UrlParams;
+use ipl\Orm\Model;
+use ipl\Stdlib\Filter;
+
+class GraphController extends Controller
+{
+ use GraphingTrait;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParamsNames = [
+ 'start', 'end',
+ 'width', 'height',
+ 'legend',
+ 'template', 'default_template',
+ 'bgcolor', 'fgcolor',
+ 'majorGridLineColor', 'minorGridLineColor'
+ ];
+
+ /**
+ * The URL parameters for metrics filtering
+ *
+ * @var UrlParams
+ */
+ protected $filterParams;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParams = [];
+
+ public function init()
+ {
+ parent::init();
+
+ $this->filterParams = clone $this->getRequest()->getUrl()->getParams();
+
+ foreach ($this->graphParamsNames as $paramName) {
+ $this->graphParams[$paramName] = $this->filterParams->shift($paramName);
+ }
+ }
+
+ public function serviceAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $serviceName = $this->filterParams->getRequired('service.name');
+ $icingadbUtils = IcingadbUtils::getInstance();
+ $query = Service::on($icingadbUtils->getDb())
+ ->with('state')
+ ->with('host');
+
+ $query->filter(Filter::all(
+ Filter::equal('service.name', $serviceName),
+ Filter::equal('service.host.name', $hostName)
+ ));
+
+ $icingadbUtils->applyRestrictions($query);
+
+ /** @var Service $service */
+ $service = $query->first();
+
+ if ($service === null) {
+ throw new HttpNotFoundException($this->translate('No such service'));
+ }
+
+ $checkCommandColumn = $service->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
+
+ $this->supplyImage(
+ $service,
+ $service->checkcommand_name,
+ $checkCommandColumn
+ );
+ }
+
+ public function hostAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $icingadbUtils = IcingadbUtils::getInstance();
+ $query = Host::on($icingadbUtils->getDb())->with('state');
+ $query->filter(Filter::equal('host.name', $hostName));
+
+ $icingadbUtils->applyRestrictions($query);
+
+ /** @var Host $host */
+ $host = $query->first();
+
+ if ($host === null) {
+ throw new HttpNotFoundException($this->translate('No such host'));
+ }
+
+ $checkCommandColumn = $host->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
+
+ $this->supplyImage(
+ $host,
+ $host->checkcommand_name,
+ $checkCommandColumn
+ );
+ }
+
+ /**
+ * Do all monitored object type independent actions
+ *
+ * @param Model $object The object to render the graphs for
+ * @param string $checkCommand The check command of the object we supply an image for
+ * @param string|null $obscuredCheckCommand The "real" check command (if any) of the object we
+ * display graphs for
+ */
+ protected function supplyImage($object, $checkCommand, $obscuredCheckCommand)
+ {
+ if (isset($this->graphParams['default_template'])) {
+ $urlParam = 'default_template';
+ $templates = $this->getAllTemplates()->getDefaultTemplates();
+ } else {
+ $urlParam = 'template';
+ $templates = $this->getAllTemplates()->getTemplates(
+ $obscuredCheckCommand === null ? $checkCommand : $obscuredCheckCommand
+ );
+ }
+
+ if (! isset($templates[$this->graphParams[$urlParam]])) {
+ throw new HttpNotFoundException($this->translate('No such template'));
+ }
+
+ $charts = $templates[$this->graphParams[$urlParam]]->getCharts(
+ static::getMetricsDataSource(),
+ $object,
+ array_map('rawurldecode', $this->filterParams->toArray(false))
+ );
+
+ switch (count($charts)) {
+ case 0:
+ throw new HttpNotFoundException($this->translate('No such graph'));
+
+ case 1:
+ $charts[0]
+ ->setFrom($this->graphParams['start'])
+ ->setUntil($this->graphParams['end'])
+ ->setWidth($this->graphParams['width'])
+ ->setHeight($this->graphParams['height'])
+ ->setBackgroundColor($this->graphParams['bgcolor'])
+ ->setForegroundColor($this->graphParams['fgcolor'])
+ ->setMajorGridLineColor($this->graphParams['majorGridLineColor'])
+ ->setMinorGridLineColor($this->graphParams['minorGridLineColor'])
+ ->setShowLegend((bool) $this->graphParams['legend'])
+ ->serveImage($this->getResponse());
+
+ // not falling through, serveImage exits
+ default:
+ throw new HttpBadRequestException('%s', $this->translate(
+ 'Graphite Web yields more than one metric for the given filter.'
+ . ' Please specify a more precise filter.'
+ ));
+ }
+ }
+}
diff --git a/application/controllers/HostsController.php b/application/controllers/HostsController.php
new file mode 100644
index 0000000..f77281a
--- /dev/null
+++ b/application/controllers/HostsController.php
@@ -0,0 +1,112 @@
+<?php
+
+/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Graphite\Web\Controller\IcingadbGraphiteController;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\IcingadbGraphs;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Web\Url;
+use ipl\Html\HtmlString;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class HostsController extends IcingadbGraphiteController
+{
+ use TimeRangePickerTrait;
+
+ public function indexAction()
+ {
+ if (! $this->useIcingadbAsBackend) {
+ $params = urldecode($this->params->get('legacyParams'));
+ $this->redirectNow(Url::fromPath('graphite/list/hosts')->setQueryString($params));
+ }
+
+ // shift graph params to avoid exception
+ $graphRange = $this->params->shift('graph_range');
+ $baseFilter = $graphRange ? Filter::equal('graph_range', $graphRange) : null;
+ foreach ($this->graphParams as $param) {
+ $this->params->shift($param);
+ }
+
+ $this->addTitleTab(t('Hosts'));
+
+ $db = $this->getDb();
+
+ $hosts = Host::on($db)->with('state');
+ $hosts->filter(Filter::like('state.performance_data', '*'));
+
+ $this->applyRestrictions($hosts);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($hosts);
+ $sortControl = $this->createSortControl($hosts, ['host.display_name' => t('Hostname')]);
+
+ $searchBar = $this->createSearchBar(
+ $hosts,
+ array_merge(
+ [$limitControl->getLimitParam(), $sortControl->getSortParam()],
+ $this->graphParams
+ )
+ );
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $hosts->filter($filter);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+ $this->handleTimeRangePickerRequest();
+ $this->addControl(HtmlString::create($this->renderTimeRangePicker($this->view)));
+
+ $this->addContent(
+ (new IcingadbGraphs($hosts->execute()))
+ ->setBaseFilter($baseFilter)
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Host::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(
+ Host::on($this->getDb()),
+ array_merge(
+ [LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM],
+ $this->graphParams
+ )
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php
new file mode 100644
index 0000000..46d8321
--- /dev/null
+++ b/application/controllers/ListController.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Module\Graphite\Web\Controller\MonitoringAwareController;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Icingadb\Compat\UrlMigrator;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Web\Filter\QueryString;
+
+class ListController extends MonitoringAwareController
+{
+ use TimeRangePickerTrait;
+
+ public function init()
+ {
+ parent::init();
+ $this->getTabs()
+ ->extend(new OutputFormat([OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON]))
+ ->extend(new DashboardAction())
+ ->extend(new MenuAction());
+ }
+
+ public function hostsAction()
+ {
+ if ($this->useIcingadbAsBackend) {
+ $legacyParams = urlencode($this->params->toString());
+ $params = QueryString::render(
+ UrlMigrator::transformFilter(
+ QueryString::parse($this->params->toString())
+ )
+ );
+
+ $url = Url::fromPath('graphite/hosts')
+ ->setQueryString($params);
+
+ if ($legacyParams) {
+ $url->setParam('legacyParams', $legacyParams);
+ }
+
+ $this->redirectNow($url);
+ }
+
+ $this->addTitleTab(
+ 'hosts',
+ mt('monitoring', 'Hosts'),
+ mt('monitoring', 'List hosts')
+ );
+
+ $hostsQuery = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('hoststatus', ['host_name'])
+ );
+
+ $hostsQuery->applyFilter(Filter::expression('host_perfdata', '!=', ''));
+
+ $this->view->baseUrl = $baseUrl = Url::fromPath('monitoring/host/show');
+ TimeRangePickerTools::copyAllRangeParameters(
+ $baseUrl->getParams(),
+ $this->getRequest()->getUrl()->getParams()
+ );
+
+ $this->filterQuery($hostsQuery);
+ $this->setupPaginationControl($hostsQuery);
+ $this->setupLimitControl();
+ $this->setupSortControl(['host_display_name' => mt('monitoring', 'Hostname')], $hostsQuery);
+
+ $hosts = [];
+ foreach ($hostsQuery->peekAhead($this->view->compact) as $host) {
+ $host = new Host($this->backend, $host->host_name);
+ $host->fetch();
+ $hosts[] = $host;
+ }
+
+ $this->handleTimeRangePickerRequest();
+ $this->view->timeRangePicker = $this->renderTimeRangePicker($this->view);
+ $this->view->hosts = $hosts;
+ $this->view->hasMoreHosts = ! $this->view->compact && $hostsQuery->hasMore();
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function servicesAction()
+ {
+ if ($this->useIcingadbAsBackend) {
+ $legacyParams = urlencode($this->params->toString());
+ $params = QueryString::render(
+ UrlMigrator::transformFilter(
+ QueryString::parse($this->params->toString())
+ )
+ );
+
+ $url = Url::fromPath('graphite/services')
+ ->setQueryString($params);
+
+ if ($legacyParams) {
+ $url->setParam('legacyParams', $legacyParams);
+ }
+
+ $this->redirectNow($url);
+ }
+
+ $this->addTitleTab(
+ 'services',
+ mt('monitoring', 'Services'),
+ mt('monitoring', 'List services')
+ );
+
+ $servicesQuery = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('servicestatus', ['host_name', 'service_description'])
+ );
+
+ $servicesQuery->applyFilter(Filter::expression('service_perfdata', '!=', ''));
+
+ $this->view->hostBaseUrl = $hostBaseUrl = Url::fromPath('monitoring/host/show');
+ TimeRangePickerTools::copyAllRangeParameters(
+ $hostBaseUrl->getParams(),
+ $this->getRequest()->getUrl()->getParams()
+ );
+
+ $this->view->serviceBaseUrl = $serviceBaseUrl = Url::fromPath('monitoring/service/show');
+ TimeRangePickerTools::copyAllRangeParameters(
+ $serviceBaseUrl->getParams(),
+ $this->getRequest()->getUrl()->getParams()
+ );
+
+ $this->filterQuery($servicesQuery);
+ $this->setupPaginationControl($servicesQuery);
+ $this->setupLimitControl();
+ $this->setupSortControl([
+ 'service_display_name' => mt('monitoring', 'Service Name'),
+ 'host_display_name' => mt('monitoring', 'Hostname')
+ ], $servicesQuery);
+
+ $services = [];
+ foreach ($servicesQuery->peekAhead($this->view->compact) as $service) {
+ $service = new Service($this->backend, $service->host_name, $service->service_description);
+ $service->fetch();
+ $services[] = $service;
+ }
+
+ $this->handleTimeRangePickerRequest();
+ $this->view->timeRangePicker = $this->renderTimeRangePicker($this->view);
+ $this->view->services = $services;
+ $this->view->hasMoreServices = ! $this->view->compact && $servicesQuery->hasMore();
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ /**
+ * Apply filters on a DataView
+ *
+ * @param DataView $dataView The DataView to apply filters on
+ */
+ protected function filterQuery(DataView $dataView)
+ {
+ $this->setupFilterControl(
+ $dataView,
+ null,
+ null,
+ array_merge(
+ ['format', 'stateType', 'addColumns', 'problems', 'graphs_limit'],
+ TimeRangePickerTools::getAllRangeParameters()
+ )
+ );
+ $this->handleFormatRequest($dataView);
+ }
+
+ /**
+ * Add title tab
+ *
+ * @param string $action
+ * @param string $title
+ * @param string $tip
+ */
+ protected function addTitleTab($action, $title, $tip)
+ {
+ $this->getTabs()->add($action, [
+ 'title' => $tip,
+ 'label' => $title,
+ 'url' => Url::fromRequest(),
+ 'active' => true
+ ]);
+ }
+}
diff --git a/application/controllers/MonitoringGraphController.php b/application/controllers/MonitoringGraphController.php
new file mode 100644
index 0000000..583c859
--- /dev/null
+++ b/application/controllers/MonitoringGraphController.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Web\Controller\MonitoringAwareController;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\UrlParams;
+
+class MonitoringGraphController extends MonitoringAwareController
+{
+ use GraphingTrait;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParamsNames = [
+ 'start', 'end',
+ 'width', 'height',
+ 'legend',
+ 'template', 'default_template',
+ 'bgcolor', 'fgcolor',
+ 'majorGridLineColor', 'minorGridLineColor'
+ ];
+
+ /**
+ * The URL parameters for metrics filtering
+ *
+ * @var UrlParams
+ */
+ protected $filterParams;
+
+ /**
+ * The URL parameters for the graph
+ *
+ * @var string[]
+ */
+ protected $graphParams = [];
+
+ public function init()
+ {
+ parent::init();
+
+ $this->filterParams = clone $this->getRequest()->getUrl()->getParams();
+
+ foreach ($this->graphParamsNames as $paramName) {
+ $this->graphParams[$paramName] = $this->filterParams->shift($paramName);
+ }
+ }
+
+ public function hostAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $checkCommandColumn = '_host_' . Graphs::getObscuredCheckCommandCustomVar();
+ $host = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('hoststatus', ['host_check_command', $checkCommandColumn])
+ )
+ ->where('host_name', $hostName)
+ ->limit(1) // just to be sure to save a few CPU cycles
+ ->fetchRow();
+
+ if ($host === false) {
+ throw new HttpNotFoundException('%s', $this->translate('No such host'));
+ }
+
+ $this->supplyImage(new Host($this->backend, $hostName), $host->host_check_command, $host->$checkCommandColumn);
+ }
+
+ public function serviceAction()
+ {
+ $hostName = $this->filterParams->getRequired('host.name');
+ $serviceName = $this->filterParams->getRequired('service.name');
+ $checkCommandColumn = '_service_' . Graphs::getObscuredCheckCommandCustomVar();
+ $service = $this->applyMonitoringRestriction(
+ $this->backend->select()->from('servicestatus', ['service_check_command', $checkCommandColumn])
+ )
+ ->where('host_name', $hostName)
+ ->where('service_description', $serviceName)
+ ->limit(1) // just to be sure to save a few CPU cycles
+ ->fetchRow();
+
+ if ($service === false) {
+ throw new HttpNotFoundException('%s', $this->translate('No such service'));
+ }
+
+ $this->supplyImage(
+ new Service($this->backend, $hostName, $serviceName),
+ $service->service_check_command,
+ $service->$checkCommandColumn
+ );
+ }
+
+ /**
+ * Do all monitored object type independend actions
+ *
+ * @param MonitoredObject $object The object to render the graphs for
+ * @param string $checkCommand The check command of the object we supply an image for
+ * @param string|null $obscuredCheckCommand The "real" check command (if any) of the object we
+ * display graphs for
+ */
+ protected function supplyImage($object, $checkCommand, $obscuredCheckCommand)
+ {
+ if (isset($this->graphParams['default_template'])) {
+ $urlParam = 'default_template';
+ $templates = $this->getAllTemplates()->getDefaultTemplates();
+ } else {
+ $urlParam = 'template';
+ $templates = $this->getAllTemplates()->getTemplates(
+ $obscuredCheckCommand === null ? $checkCommand : $obscuredCheckCommand
+ );
+ }
+
+ if (! isset($templates[$this->graphParams[$urlParam]])) {
+ throw new HttpNotFoundException($this->translate('No such template'));
+ }
+
+ $charts = $templates[$this->graphParams[$urlParam]]->getCharts(
+ static::getMetricsDataSource(),
+ $object,
+ array_map('rawurldecode', $this->filterParams->toArray(false))
+ );
+
+ switch (count($charts)) {
+ case 0:
+ throw new HttpNotFoundException($this->translate('No such graph'));
+
+ case 1:
+ $charts[0]
+ ->setFrom($this->graphParams['start'])
+ ->setUntil($this->graphParams['end'])
+ ->setWidth($this->graphParams['width'])
+ ->setHeight($this->graphParams['height'])
+ ->setBackgroundColor($this->graphParams['bgcolor'])
+ ->setForegroundColor($this->graphParams['fgcolor'])
+ ->setMajorGridLineColor($this->graphParams['majorGridLineColor'])
+ ->setMinorGridLineColor($this->graphParams['minorGridLineColor'])
+ ->setShowLegend((bool) $this->graphParams['legend'])
+ ->serveImage($this->getResponse());
+
+ // not falling through, serveImage exits
+ default:
+ throw new HttpBadRequestException('%s', $this->translate(
+ 'Graphite Web yields more than one metric for the given filter.'
+ . ' Please specify a more precise filter.'
+ ));
+ }
+ }
+}
diff --git a/application/controllers/ServicesController.php b/application/controllers/ServicesController.php
new file mode 100644
index 0000000..212ad1f
--- /dev/null
+++ b/application/controllers/ServicesController.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Icinga\Module\Graphite\Controllers;
+
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Module\Graphite\Web\Controller\IcingadbGraphiteController;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\IcingadbGraphs;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions;
+use Icinga\Web\Url;
+use ipl\Html\HtmlString;
+use ipl\Stdlib\Filter;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\SortControl;
+
+class ServicesController extends IcingadbGraphiteController
+{
+ use TimeRangePickerTrait;
+
+ public function indexAction()
+ {
+ if (! $this->useIcingadbAsBackend) {
+ $params = urldecode($this->params->get('legacyParams'));
+ $this->redirectNow(Url::fromPath('graphite/list/services')->setQueryString($params));
+ }
+
+ // shift graph params to avoid exception
+ $graphRange = $this->params->shift('graph_range');
+ $baseFilter = $graphRange ? Filter::equal('graph_range', $graphRange) : null;
+ foreach ($this->graphParams as $param) {
+ $this->params->shift($param);
+ }
+
+ $this->addTitleTab(t('Services'));
+
+ $db = $this->getDb();
+
+ $services = Service::on($db)
+ ->with('state')
+ ->with('host');
+ $services->filter(Filter::like('state.performance_data', '*'));
+
+ $this->applyRestrictions($services);
+
+ $limitControl = $this->createLimitControl();
+ $paginationControl = $this->createPaginationControl($services);
+ $sortControl = $this->createSortControl($services, [
+ 'service.display_name' => t('Servicename'),
+ 'host.display_name' => t('Hostname')
+ ]);
+
+ $searchBar = $this->createSearchBar(
+ $services,
+ array_merge(
+ [$limitControl->getLimitParam(), $sortControl->getSortParam()],
+ $this->graphParams
+ )
+ );
+
+ if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
+ if ($searchBar->hasBeenSubmitted()) {
+ $filter = $this->getFilter();
+ } else {
+ $this->addControl($searchBar);
+ $this->sendMultipartUpdate();
+ return;
+ }
+ } else {
+ $filter = $searchBar->getFilter();
+ }
+
+ $services->filter($filter);
+
+ $this->addControl($paginationControl);
+ $this->addControl($sortControl);
+ $this->addControl($limitControl);
+ $this->addControl($searchBar);
+ $this->handleTimeRangePickerRequest();
+ $this->addControl(HtmlString::create($this->renderTimeRangePicker($this->view)));
+
+ $this->addContent(
+ (new IcingadbGraphs($services->execute()))
+ ->setBaseFilter($baseFilter)
+ );
+
+ if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
+ $this->sendMultipartUpdate();
+ }
+
+ $this->setAutorefreshInterval(30);
+ }
+
+ public function completeAction()
+ {
+ $suggestions = new ObjectSuggestions();
+ $suggestions->setModel(Service::class);
+ $suggestions->forRequest(ServerRequest::fromGlobals());
+ $this->getDocument()->add($suggestions);
+ }
+
+ public function searchEditorAction()
+ {
+ $editor = $this->createSearchEditor(
+ Service::on($this->getDb()),
+ array_merge(
+ [LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM],
+ $this->graphParams
+ )
+ );
+
+ $this->getDocument()->add($editor);
+ $this->setTitle(t('Adjust Filter'));
+ }
+}
diff --git a/application/forms/Config/AdvancedForm.php b/application/forms/Config/AdvancedForm.php
new file mode 100644
index 0000000..1fc196b
--- /dev/null
+++ b/application/forms/Config/AdvancedForm.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\Config;
+
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Graphite\Web\Form\Validator\MacroTemplateValidator;
+use Zend_Validate_Regex;
+
+class AdvancedForm extends ConfigForm
+{
+ public function init()
+ {
+ $this->setName('form_config_graphite_advanced');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ [
+ 'number',
+ 'ui_default_time_range',
+ [
+ 'label' => $this->translate('Default time range'),
+ 'description' => $this->translate('The default time range for graphs'),
+ 'min' => 0,
+ 'value' => 1
+ ]
+ ],
+ [
+ 'select',
+ 'ui_default_time_range_unit',
+ [
+ 'label' => $this->translate('Default time range unit'),
+ 'description' => $this->translate('The above range\'s unit'),
+ 'multiOptions' => [
+ 'minutes' => $this->translate('Minutes'),
+ 'hours' => $this->translate('Hours'),
+ 'days' => $this->translate('Days'),
+ 'weeks' => $this->translate('Weeks'),
+ 'months' => $this->translate('Months'),
+ 'years' => $this->translate('Years')
+ ],
+ 'value' => 'hours'
+ ]
+ ],
+ [
+ 'checkbox',
+ 'ui_disable_no_graphs_found',
+ [
+ 'label' => $this->translate('Disable "no graphs found"'),
+ 'description' => $this->translate(
+ 'If no graphs were found for a monitored object, just display nothing at all'
+ ),
+ ]
+ ],
+ [
+ 'text',
+ 'icinga_graphite_writer_host_name_template',
+ [
+ 'label' => $this->translate('Host name template'),
+ 'description' => $this->translate(
+ 'The value of your Icinga 2 GraphiteWriter\'s'
+ . ' attribute host_name_template (if specified)'
+ ),
+ 'validators' => [new MacroTemplateValidator()]
+ ]
+ ],
+ [
+ 'text',
+ 'icinga_graphite_writer_service_name_template',
+ [
+ 'label' => $this->translate('Service name template'),
+ 'description' => $this->translate(
+ 'The value of your Icinga 2 GraphiteWriter\'s'
+ . ' attribute service_name_template (if specified)'
+ ),
+ 'validators' => [new MacroTemplateValidator()]
+ ]
+ ],
+ [
+ 'text',
+ 'icinga_customvar_obscured_check_command',
+ [
+ 'label' => $this->translate('Obscured check command custom variable'),
+ 'description' => $this->translate(
+ 'The Icinga custom variable with the "actual" check command obscured'
+ . ' by e.g. check_by_ssh (defaults to check_command)'
+ ),
+ 'validators' => [new Zend_Validate_Regex('/\A\w*\z/')]
+ ]
+ ]
+ ]);
+ }
+}
diff --git a/application/forms/Config/BackendForm.php b/application/forms/Config/BackendForm.php
new file mode 100644
index 0000000..90e0af2
--- /dev/null
+++ b/application/forms/Config/BackendForm.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\Config;
+
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Graphite\Web\Form\Validator\HttpUserValidator;
+
+class BackendForm extends ConfigForm
+{
+ public function init()
+ {
+ $this->setName('form_config_graphite_backend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ [
+ 'text',
+ 'graphite_url',
+ [
+ 'required' => true,
+ 'label' => $this->translate('Graphite Web URL'),
+ 'description' => $this->translate('URL to your Graphite Web'),
+ 'validators' => ['UrlValidator']
+ ]
+ ],
+ [
+ 'text',
+ 'graphite_user',
+ [
+ 'label' => $this->translate('Graphite Web user'),
+ 'description' => $this->translate(
+ 'A user with access to your Graphite Web via HTTP basic authentication'
+ ),
+ 'validators' => [new HttpUserValidator()]
+ ]
+ ],
+ [
+ 'password',
+ 'graphite_password',
+ [
+ 'renderPassword' => true,
+ 'label' => $this->translate('Graphite Web password'),
+ 'description' => $this->translate('The above user\'s password')
+ ]
+ ],
+ [
+ 'checkbox',
+ 'graphite_insecure',
+ [
+ 'label' => $this->translate('Connect insecurely'),
+ 'description' => $this->translate('Check this to not verify the remote\'s TLS certificate')
+ ]
+ ]
+ ]);
+ }
+}
diff --git a/application/forms/TimeRangePicker/CommonForm.php b/application/forms/TimeRangePicker/CommonForm.php
new file mode 100644
index 0000000..21e2096
--- /dev/null
+++ b/application/forms/TimeRangePicker/CommonForm.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
+
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Web\Form;
+use Zend_Form_Element_Select;
+
+class CommonForm extends Form
+{
+ /**
+ * The selectable units with themselves in seconds
+ *
+ * One month equals 30 days and one year equals 365.25 days. This should cover enough cases.
+ *
+ * @var int[]
+ */
+ protected $rangeFactors = [
+ 'minutes' => 60,
+ 'hours' => 3600,
+ 'days' => 86400,
+ 'weeks' => 604800,
+ 'months' => 2592000,
+ 'years' => 31557600
+ ];
+
+ /**
+ * The elements' default values
+ *
+ * @var string[]|null
+ */
+ protected $defaultFormData;
+
+ public function init()
+ {
+ $this->setName('form_timerangepickercommon_graphite');
+ $this->setAttrib('data-base-target', '_self');
+ $this->setAttrib('class', 'icinga-form icinga-controls inline');
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ $this->createSelect(
+ 'minutes',
+ $this->translate('Minutes'),
+ $this->translate('Show the last … minutes'),
+ [5, 10, 15, 30, 45],
+ $this->translate('%d minute'),
+ $this->translate('%d minutes')
+ ),
+ $this->createSelect(
+ 'hours',
+ $this->translate('Hours'),
+ $this->translate('Show the last … hours'),
+ [1, 2, 3, 6, 12, 18],
+ $this->translate('%d hour'),
+ $this->translate('%d hours')
+ ),
+ $this->createSelect(
+ 'days',
+ $this->translate('Days'),
+ $this->translate('Show the last … days'),
+ range(1, 6),
+ $this->translate('%d day'),
+ $this->translate('%d days')
+ ),
+ $this->createSelect(
+ 'weeks',
+ $this->translate('Weeks'),
+ $this->translate('Show the last … weeks'),
+ range(1, 4),
+ $this->translate('%d week'),
+ $this->translate('%d weeks')
+ ),
+ $this->createSelect(
+ 'months',
+ $this->translate('Months'),
+ $this->translate('Show the last … months'),
+ [1, 2, 3, 6, 9],
+ $this->translate('%d month'),
+ $this->translate('%d months')
+ ),
+ $this->createSelect(
+ 'years',
+ $this->translate('Years'),
+ $this->translate('Show the last … years'),
+ range(1, 3),
+ $this->translate('%d year'),
+ $this->translate('%d years')
+ )
+ ]);
+
+ $this->urlToForm();
+
+ $this->defaultFormData = $this->getValues();
+ }
+
+ public function onSuccess()
+ {
+ $this->formToUrl();
+ $this->getRedirectUrl()->remove(array_values(TimeRangePickerTools::getAbsoluteRangeParameters()));
+ }
+
+ /**
+ * Create a common range picker for a specific time unit
+ *
+ * @param string $name
+ * @param string $label
+ * @param string $description
+ * @param int[] $options
+ * @param string $singular
+ * @param string $plural
+ *
+ * @return Zend_Form_Element_Select
+ */
+ protected function createSelect($name, $label, $description, array $options, $singular, $plural)
+ {
+ $multiOptions = ['' => $label];
+ foreach ($options as $option) {
+ $multiOptions[$option] = sprintf($option === 1 ? $singular : $plural, $option);
+ }
+
+ $element = $this->createElement('select', $name, [
+ 'label' => $label,
+ 'description' => $description,
+ 'multiOptions' => $multiOptions,
+ 'title' => $description,
+ 'autosubmit' => true
+ ]);
+
+ $decorators = $element->getDecorators();
+ $element->setDecorators([
+ 'Zend_Form_Decorator_ViewHelper' => $decorators['Zend_Form_Decorator_ViewHelper']
+ ]);
+
+ return $element;
+ }
+
+ /**
+ * Set this form's elements' default values based on the redirect URL's parameters
+ */
+ protected function urlToForm()
+ {
+ $params = $this->getRedirectUrl()->getParams();
+ $seconds = TimeRangePickerTools::getRelativeSeconds($params);
+
+ if (
+ $seconds === null
+ && count(array_intersect_key(
+ $params->toArray(false),
+ array_keys(TimeRangePickerTools::getAllRangeParameters())
+ )) === 0
+ ) {
+ $seconds = TimeRangePickerTools::getDefaultRelativeTimeRange();
+ }
+
+ if ($seconds !== null) {
+ if ($seconds !== false) {
+ foreach ($this->rangeFactors as $unit => $factor) {
+ /** @var Zend_Form_Element_Select $element */
+ $element = $this->getElement($unit);
+
+ $options = $element->getMultiOptions();
+ unset($options['']);
+
+ foreach ($options as $option => $_) {
+ if ($seconds === $option * $factor) {
+ $element->setValue((string) $option);
+ return;
+ }
+ }
+ }
+ }
+
+ $params->remove(TimeRangePickerTools::getRelativeRangeParameter());
+ }
+ }
+
+ /**
+ * Change the redirect URL's parameters based on this form's elements' values
+ */
+ protected function formToUrl()
+ {
+ $formData = $this->getValues();
+ foreach ($this->rangeFactors as $unit => $factor) {
+ if ($formData[$unit] !== '' && $formData[$unit] !== $this->defaultFormData[$unit]) {
+ $this->getRedirectUrl()->setParam(
+ TimeRangePickerTools::getRelativeRangeParameter(),
+ (string) ((int) $formData[$unit] * $factor)
+ );
+ return;
+ }
+ }
+ }
+}
diff --git a/application/forms/TimeRangePicker/CustomForm.php b/application/forms/TimeRangePicker/CustomForm.php
new file mode 100644
index 0000000..89b5833
--- /dev/null
+++ b/application/forms/TimeRangePicker/CustomForm.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace Icinga\Module\Graphite\Forms\TimeRangePicker;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Module\Graphite\Web\Form\Decorator\Proxy;
+use Icinga\Web\Form;
+
+class CustomForm extends Form
+{
+ /**
+ * @var string
+ */
+ protected $dateTimeFormat = 'Y-m-d\TH:i';
+
+ /**
+ * @var string
+ */
+ protected $timestamp = '/^(?:0|-?[1-9]\d*)$/';
+
+ /**
+ * Right now
+ *
+ * @var DateTime
+ */
+ protected $now;
+
+ public function init()
+ {
+ $this->setName('form_timerangepickercustom_graphite');
+ $this->setAttrib('data-base-target', '_self');
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElements([
+ [
+ 'date',
+ 'start_date',
+ [
+ 'placeholder' => 'YYYY-MM-DD',
+ 'label' => $this->translate('Start'),
+ 'description' => $this->translate('Start of the date/time range')
+ ]
+ ],
+ [
+ 'time',
+ 'start_time',
+ [
+ 'placeholder' => 'HH:MM',
+ 'label' => $this->translate('Start'),
+ 'description' => $this->translate('Start of the date/time range')
+ ]
+ ],
+ [
+ 'date',
+ 'end_date',
+ [
+ 'placeholder' => 'YYYY-MM-DD',
+ 'label' => $this->translate('End'),
+ 'description' => $this->translate('End of the date/time range')
+ ]
+ ],
+ [
+ 'time',
+ 'end_time',
+ [
+ 'placeholder' => 'HH:MM',
+ 'label' => $this->translate('End'),
+ 'description' => $this->translate('End of the date/time range')
+ ]
+ ]
+ ]);
+
+ $this->groupDateTime('start');
+ $this->groupDateTime('end');
+
+ $this->setSubmitLabel($this->translate('Update'));
+
+ $this->urlToForm('start', $this->getRelativeTimestamp());
+ $this->urlToForm('end');
+ }
+
+ public function addSubmitButton()
+ {
+ $result = parent::addSubmitButton();
+
+ $this->getElement('btn_submit')->class = 'flyover-toggle';
+
+ return $result;
+ }
+
+ public function onSuccess()
+ {
+ $start = $this->formToUrl('start', '00:00');
+ $end = $this->formToUrl('end', '23:59', 'PT59S');
+ if ($start > $end) {
+ $absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
+ $this->getRedirectUrl()->getParams()
+ ->set($absoluteRangeParameters['start'], $end)
+ ->set($absoluteRangeParameters['end'], $start);
+ }
+
+ $this->getRedirectUrl()->remove(TimeRangePickerTools::getRelativeRangeParameter());
+ }
+
+ /**
+ * Add display group for a date and a time input belonging together
+ *
+ * @param string $part Either 'start' or 'end'
+ */
+ protected function groupDateTime($part)
+ {
+ $this->addDisplayGroup(["{$part}_date", "{$part}_time"], $part);
+ $group = $this->getDisplayGroup($part);
+
+ foreach ($group->getElements() as $element) {
+ /** @var \Zend_Form_Element $element */
+
+ $elementDecorators = $element->getDecorators();
+ $element->setDecorators([
+ 'Zend_Form_Decorator_ViewHelper' => $elementDecorators['Zend_Form_Decorator_ViewHelper']
+ ]);
+ }
+
+ $decorators = [];
+ foreach ($elementDecorators as $key => $decorator) {
+ if ($key === 'Zend_Form_Decorator_ViewHelper') {
+ $decorators['Zend_Form_Decorator_FormElements'] =
+ $group->getDecorators()['Zend_Form_Decorator_FormElements'];
+ } else {
+ $decorators[$key] = (new Proxy())->setActualDecorator($decorator->setElement($element));
+ }
+ }
+
+ $group->setDecorators($decorators);
+ }
+
+ /**
+ * Set this form's elements' default values based on the redirect URL's parameters
+ *
+ * @param string $part Either 'start' or 'end'
+ * @param int $defaultTimestamp Fallback
+ */
+ protected function urlToForm($part, $defaultTimestamp = null)
+ {
+ $params = $this->getRedirectUrl()->getParams();
+ $absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
+ $timestamp = $params->get($absoluteRangeParameters[$part], $defaultTimestamp);
+
+ if ($timestamp !== null) {
+ if (preg_match($this->timestamp, $timestamp)) {
+ list($date, $time) = explode(
+ 'T',
+ DateTime::createFromFormat('U', $timestamp)
+ ->setTimezone(new DateTimeZone(date_default_timezone_get()))
+ ->format($this->dateTimeFormat)
+ );
+
+ $this->getElement("{$part}_date")->setValue($date);
+ $this->getElement("{$part}_time")->setValue($time);
+ } else {
+ $params->remove($absoluteRangeParameters[$part]);
+ }
+ }
+ }
+
+ /**
+ * Get the relative range start (if any) set by {@link CommonForm}
+ *
+ * @return int|null
+ */
+ protected function getRelativeTimestamp()
+ {
+ $seconds = TimeRangePickerTools::getRelativeSeconds($this->getRedirectUrl()->getParams());
+ return is_int($seconds) ? $this->getNow()->getTimestamp() - $seconds : null;
+ }
+
+ /**
+ * Change the redirect URL's parameters based on this form's elements' values
+ *
+ * @param string $part Either 'start' or 'end'
+ * @param string $defaultTime Default if no time given
+ * @param string $addInterval Add this interval to the result
+ *
+ * @return int|null The updated timestamp (if any)
+ */
+ protected function formToUrl($part, $defaultTime, $addInterval = null)
+ {
+ $date = $this->getValue("{$part}_date");
+ $time = $this->getValue("{$part}_time");
+ $params = $this->getRedirectUrl()->getParams();
+ $absoluteRangeParameters = TimeRangePickerTools::getAbsoluteRangeParameters();
+
+ if ($date === '' && $time === '') {
+ $params->remove($absoluteRangeParameters[$part]);
+ } else {
+ $dateTime = DateTime::createFromFormat(
+ $this->dateTimeFormat,
+ ($date === '' ? $this->getNow()->format('Y-m-d') : $date)
+ . 'T' . ($time === '' ? $defaultTime : $time)
+ );
+
+ if ($dateTime === false) {
+ $params->remove($absoluteRangeParameters[$part]);
+ } else {
+ if ($addInterval !== null) {
+ $dateTime->add(new DateInterval($addInterval));
+ }
+
+ $params->set($absoluteRangeParameters[$part], $dateTime->format('U'));
+ return $dateTime->getTimestamp();
+ }
+ }
+ }
+
+ /**
+ * Get {@link now}
+ *
+ * @return DateTime
+ */
+ public function getNow()
+ {
+ if ($this->now === null) {
+ $this->now = new DateTime();
+ }
+
+ return $this->now;
+ }
+
+ /**
+ * Set {@link now}
+ *
+ * @param DateTime $now
+ *
+ * @return $this
+ */
+ public function setNow($now)
+ {
+ $this->now = $now;
+ return $this;
+ }
+}
diff --git a/application/views/scripts/config/advanced.phtml b/application/views/scripts/config/advanced.phtml
new file mode 100644
index 0000000..ab47cdb
--- /dev/null
+++ b/application/views/scripts/config/advanced.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+
+<div class="content">
+ <?= /** @var \Icinga\Module\Graphite\Forms\Config\AdvancedForm $form */ $form ?>
+</div>
diff --git a/application/views/scripts/config/backend.phtml b/application/views/scripts/config/backend.phtml
new file mode 100644
index 0000000..7750f3c
--- /dev/null
+++ b/application/views/scripts/config/backend.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+
+<div class="content">
+ <?= /** @var \Icinga\Module\Graphite\Forms\Config\BackendForm $form */ $form ?>
+</div>
diff --git a/application/views/scripts/list/hosts.phtml b/application/views/scripts/list/hosts.phtml
new file mode 100644
index 0000000..ce0e37c
--- /dev/null
+++ b/application/views/scripts/list/hosts.phtml
@@ -0,0 +1,67 @@
+<?php
+
+use Icinga\Module\Graphite\Web\Widget\Graphs\Host;
+use Icinga\Web\Url;
+
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Web\Widget\FilterEditor $filterEditor */
+/** @var \Icinga\Module\Monitoring\Object\Host[] $hosts */
+/** @var bool $hasMoreHosts */
+/** @var \Icinga\Web\Url $baseUrl */
+
+if (! $compact): ?>
+<div class="controls">
+ <?= $tabs ?>
+ <?= $paginator ?>
+ <div class="sort-controls-container">
+ <?= $limiter ?>
+ <?= $sortBox ?>
+ </div>
+ <?= $filterEditor ?>
+ <?= $timeRangePicker ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+if (! empty($hosts)) {
+ echo '<div class="graphite-graph-color-registry"></div>';
+ echo '<div class="grid">';
+ foreach ($hosts as $host) {
+ $hostGraphs = (string) (new Host($host))->setPreloadDummy()->handleRequest();
+
+ if ($hostGraphs !== '') {
+ echo '<div class="grid-item">'
+ . '<h2>'
+ . $this->qlink(
+ $host->host_name === $host->host_display_name
+ ? $host->host_display_name
+ : $host->host_display_name . ' (' . $this->escape($host->host_name) . ')',
+ $baseUrl->with(['host' => $host->host_name]),
+ null,
+ ['data-base-target' => '_next']
+ )
+ . '</h2>'
+ . $hostGraphs
+ . '</div>';
+ }
+ }
+
+ if ($hasMoreHosts) {
+ echo '<div class="action-links">'
+ . $this->qlink(
+ mt('monitoring', 'Show More'),
+ $this->url()->without(array('view', 'limit')),
+ null,
+ [
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ ]
+ )
+ . '</div>';
+ }
+ echo '</div>';
+} else {
+ echo '<p>' . $this->escape(mt('monitoring', 'No hosts found matching the filter.')) . '</p>';
+}
+?>
+</div>
diff --git a/application/views/scripts/list/services.phtml b/application/views/scripts/list/services.phtml
new file mode 100644
index 0000000..90ca03c
--- /dev/null
+++ b/application/views/scripts/list/services.phtml
@@ -0,0 +1,77 @@
+<?php
+
+use Icinga\Module\Graphite\Web\Widget\Graphs\Service;
+use Icinga\Web\Url;
+
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Web\Widget\FilterEditor $filterEditor */
+/** @var \Icinga\Module\Monitoring\Object\Service[] $services */
+/** @var bool $hasMoreServices */
+/** @var \Icinga\Web\Url $hostBaseUrl */
+/** @var \Icinga\Web\Url $serviceBaseUrl */
+
+if (! $compact): ?>
+<div class="controls">
+ <?= $tabs ?>
+ <?= $paginator ?>
+ <div class="sort-controls-container">
+ <?= $limiter ?>
+ <?= $sortBox ?>
+ </div>
+ <?= $filterEditor ?>
+ <?= $timeRangePicker ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+if (! empty($services)) {
+ echo '<div class="graphite-graph-color-registry"></div>';
+ echo '<div class="grid">';
+ foreach ($services as $service) {
+ echo '<div class="grid-item">'
+ . '<h2>'
+ . $this->qlink(
+ $service->host_name === $service->host_display_name
+ ? $service->host_display_name
+ : $service->host_display_name . ' (' . $this->escape($service->host_name) . ')',
+ $hostBaseUrl->with(['host' => $service->host_name]),
+ null,
+ ['data-base-target' => '_next']
+ )
+ . '&#58; '
+ . $this->qlink(
+ $service->service_description === $service->service_display_name
+ ? $service->service_display_name
+ : $service->service_display_name . ' (' . $this->escape($service->service_description) . ')',
+ $serviceBaseUrl->with([
+ 'host' => $service->host_name,
+ 'service' => $service->service_description
+ ]),
+ null,
+ ['data-base-target' => '_next']
+ )
+ . '</h2>';
+
+ echo (new Service($service))->setPreloadDummy()->handleRequest();
+ echo '</div>';
+ }
+
+ if ($hasMoreServices) {
+ echo '<div class="action-links">'
+ . $this->qlink(
+ mt('monitoring', 'Show More'),
+ $this->url()->without(array('view', 'limit')),
+ null,
+ [
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ ]
+ )
+ . '</div>';
+ }
+ echo '</div>';
+} else {
+ echo '<p>' . $this->escape(mt('monitoring', 'No services found matching the filter.')) . '</p>';
+}
+?>
+</div>
diff --git a/application/views/scripts/test/apache.phtml b/application/views/scripts/test/apache.phtml
new file mode 100644
index 0000000..069ccbe
--- /dev/null
+++ b/application/views/scripts/test/apache.phtml
@@ -0,0 +1,12 @@
+<div class="controls">
+<?= $this->tabs ?>
+</div>
+
+<div class="content">
+<?php foreach ($this->images as $base => $img): ?>
+<div style="width: 260px; float: left; margin-right: 5px;">
+<h3><?= $this->escape($base) ?></h3>
+<img src="<?= $img ?>" />
+</div>
+<?php endforeach ?>
+</div>
diff --git a/application/views/scripts/test/cpu.phtml b/application/views/scripts/test/cpu.phtml
new file mode 100644
index 0000000..495e315
--- /dev/null
+++ b/application/views/scripts/test/cpu.phtml
@@ -0,0 +1,28 @@
+<?php
+$maxCnt = 0;
+foreach ($this->images as $base => $cpus) {
+ $maxCnt = max($maxCnt, count($cpus));
+}
+?>
+<div class="controls">
+<?= $this->tabs ?>
+<h1>CPUs</h1>
+</div>
+<div class="content">
+<table style="width: 100%;">
+<tr>
+ <th style="width: 15em;">&nbsp;</th>
+ <th>CPUs</th>
+</tr>
+<?php foreach ($this->images as $base => $cpus): ?>
+<tr>
+<th style="vertical-align: top; text-align: right; padding-right: 2em;"><?= $this->escape($base) ?></th>
+<td>
+<?php foreach ($cpus as $num => $img): ?>
+<div style="width: 53px; float: left;"><img src="<?= $img ?>" /><!--<br />CPU <?= $num ?>--></div>
+<?php endforeach ?>
+</td>
+</tr>
+<?php endforeach ?>
+</table>
+</div>
diff --git a/configuration.php b/configuration.php
new file mode 100644
index 0000000..1ac05b3
--- /dev/null
+++ b/configuration.php
@@ -0,0 +1,31 @@
+<?php
+
+/** @var \Icinga\Application\Modules\Module $this */
+
+/** @var \Icinga\Application\Modules\MenuItemContainer $section */
+
+use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport;
+
+$section = $this->menuSection(N_('Graphite'), ['icon' => 'chart-area']);
+
+if ($this::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
+ $section->add(N_('Hosts'), ['url' => 'graphite/hosts']);
+ $section->add(N_('Services'), ['url' => 'graphite/services']);
+} else {
+ $section->add(N_('Hosts'), ['url' => 'graphite/list/hosts']);
+ $section->add(N_('Services'), ['url' => 'graphite/list/services']);
+}
+
+$this->provideConfigTab('backend', array(
+ 'title' => $this->translate('Configure the Graphite Web backend'),
+ 'label' => $this->translate('Backend'),
+ 'url' => 'config/backend'
+));
+
+$this->provideConfigTab('advanced', array(
+ 'title' => $this->translate('Advanced configuration'),
+ 'label' => $this->translate('Advanced'),
+ 'url' => 'config/advanced'
+));
+
+$this->providePermission('graphite/debug', $this->translate('Allow debugging directly via the web UI'));
diff --git a/doc/01-About.md b/doc/01-About.md
new file mode 100644
index 0000000..01e012f
--- /dev/null
+++ b/doc/01-About.md
@@ -0,0 +1,19 @@
+# Icinga Web Graphite Integration
+
+This module integrates an existing [Graphite](https://graphite.readthedocs.io/en/latest/)
+installation in your [Icinga Web](https://icinga.com/products/infrastructure-monitoring/)
+frontend.
+
+![Service List](img/service-list.png) | ![Detail View](img/service-detail-view.png)
+--------------------------------------|--------------------------------------------
+
+It provides a new menu section with two general overviews for hosts and
+services as well as an extension to the host and service detail view of
+the monitoring module.
+
+## Documentation
+
+* [Installation](02-Installation.md)
+* [Configuration](03-Configuration.md)
+* [Templates](04-Templates.md)
+* [Troubleshooting](05-Troubleshooting.md)
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
new file mode 100644
index 0000000..6850ecd
--- /dev/null
+++ b/doc/02-Installation.md
@@ -0,0 +1,48 @@
+<!-- {% if index %} -->
+# Installing Icinga Web Graphite Integration
+
+It is recommended to use prebuilt packages
+for all supported platforms from our official release repository.
+Of course [Icinga Web](https://icinga.com/docs/icinga-web) itself
+is required to run its Graphite integration.
+The latter uses Graphite Web, so that is required as well.
+If they are not already set up, it is best to do this first.
+
+The following steps will guide you through installing
+and setting up Icinga Web Graphite Integration.
+<!-- {% else %} -->
+<!-- {% if not icingaDocs %} -->
+
+## Installing the Package
+
+If the [repository](https://packages.icinga.com) is not configured yet, please add it first.
+Then use your distribution's package manager to install the `icinga-graphite` package
+or install [from source](02-Installation.md.d/From-Source.md).
+<!-- {% endif %} --><!-- {# end if not icingaDocs #} -->
+
+## Prepare Icinga 2
+
+Enable the graphite feature:
+
+```
+# icinga2 feature enable graphite
+```
+
+Adjust its configuration in `/etc/icinga2/features-enabled/graphite.conf`:
+
+```
+object GraphiteWriter "graphite" {
+ host = "192.0.2.42"
+ port = 2003
+ enable_send_thresholds = true
+}
+```
+
+And then restart Icinga2. Enabling thresholds is not a hard requirement.
+However, some templates look better if they are able to render a max
+value or similar.
+
+## Configuring the Icinga Web Graphite Integration
+
+For required additional steps see the [Configuration](03-Configuration.md) chapter.
+<!-- {% endif %} --><!-- {# end else if index #} -->
diff --git a/doc/02-Installation.md.d/From-Source.md b/doc/02-Installation.md.d/From-Source.md
new file mode 100644
index 0000000..1433743
--- /dev/null
+++ b/doc/02-Installation.md.d/From-Source.md
@@ -0,0 +1,14 @@
+# Installing Icinga Web Graphite Integration from Source
+
+Please see the Icinga Web documentation on
+[how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source.
+Make sure you use `graphite` 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)
+* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥0.11)
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md
new file mode 100644
index 0000000..0b2a38d
--- /dev/null
+++ b/doc/03-Configuration.md
@@ -0,0 +1,65 @@
+# <a id="Configuration"></a>Configuration
+
+## Basics
+
+Open up the Icinga Web frontend and navigate to:
+
+`Configuration > Modules > graphite > Backend`
+
+Enter the Graphite Web URL. (e.g. `https://192.0.2.42:8003/`)
+
+The HTTP basic authentication credentials are only required
+if your Graphite Web is protected by such a mechanism.
+
+## Advanced
+
+Open up the Icinga Web frontend and navigate to:
+
+`Configuration > Modules > graphite > Advanced`
+
+### UI
+
+The settings *Default time range* and *Default time range unit* set the default
+time range for displayed graphs both in the graphs lists and in monitored
+objects' detail views.
+
+If you'd like to suppress the *No graphs found* messages, check *Disable "no
+graphs found"*. (This may cause unexpected blank pages in the graphs lists.)
+
+### Icinga 2 (Core)
+
+The settings *Host name template* and *Service name template* both are only
+required if you are using a different naming schema than the default Icinga 2
+is using. (As outlined [here](https://www.icinga.com/docs/icinga2/latest/doc/14-features/#current-graphite-schema))
+
+The setting *Obscured check command custom variable* is only required if there
+are wrapped check commands (see below) and the "actual" check command is stored
+in another custom variable than `check_command`.
+
+## Wrapped check commands
+
+If a monitored object is checked remotely and not via an Icinga 2 agent, but
+e.g. by check_by_ssh or check_nrpe, the monitored object's effective check
+command becomes by_ssh or nrpe respectively. This breaks the respective
+monitored objects' graphs as graph templates are applied to monitored objects
+via their check commands. (They fall back to the default template.)
+
+To make the respective graphs working as expected you have to tell the
+monitored object's "actual" check command by setting its custom variable
+`check_command`, e.g.:
+
+```
+apply Service "by_ssh-disk" {
+ import "generic-service"
+ check_command = "by_ssh"
+ vars.by_ssh_address = "192.0.2.1"
+ vars.by_ssh_command = "/usr/lib64/nagios/plugins/check_disk -w 20 -c 10"
+ vars.check_command = "disk" // <= HERE
+ assign where host.name == NodeName
+}
+```
+
+## Further reading
+
+* [Templates](04-Templates.md)
+* [Troubleshooting](05-Troubleshooting.md)
diff --git a/doc/04-Templates.md b/doc/04-Templates.md
new file mode 100644
index 0000000..5acbae1
--- /dev/null
+++ b/doc/04-Templates.md
@@ -0,0 +1,226 @@
+# Templates <a id="templates"></a>
+
+A template defines what kind of data a graph visualizes, which kind of graph to
+use and its style. Essentially the Icinga Web Graphite Integration is using
+templates to tell Graphite how to render which graphs.
+
+* [Location](04-Templates.md#templates-location)
+* [Structure](04-Templates.md#templates-structure)
+ * [graph](04-Templates.md#templates-structure-graph)
+ * [metric_filters](04-Templates.md#templates-structure-metric-filters)
+ * [urlparams](04-Templates.md#templates-structure-urlparams)
+ * [functions](04-Templates.md#templates-structure-functions)
+* [Example](04-Templates.md#templates-example)
+* [Default Template Settings](04-Templates.md#templates-default-settings)
+
+## Template Location <a id="templates-location"></a>
+
+There are a bunch of templates already included, located in
+the installation path. (e.g. `/usr/share/icingaweb2/modules/graphite`)
+
+To add additional/customized templates, place them in its configuration path.
+(e.g. `/etc/icingaweb2/modules/graphite/templates`) These will either extend
+the available templates or override some of them. Subfolders placed here will
+also be included in the same way, while additionally extending or overriding
+templates of its parent folders.
+
+> **Note:**
+>
+> Hidden files and directories (with a leading dot) are ignored.
+
+## Template Structure <a id="templates-structure"></a>
+
+Templates are organized within simple INI files. However, it is perfectly valid
+to define multiple templates in a single file.
+
+The name of a section consists of two parts separated by a dot:
+
+ [hostalive-rta.graph]
+
+The first part is the name of the template and the second part the name of one
+of the following configuration topics:
+
+> **Note:**
+>
+> Template file will be ignored if the [graph] or [metric_filters] section is missing.
+
+### Template Structure: graph <a id="templates-structure-graph"></a>
+
+Supports a single option called `check_command` and should be set to the name
+of a Icinga 2 [check-command](https://www.icinga.com/docs/icinga2/latest/doc/03-monitoring-basics/#check-commands).
+To get multiple graphs for hosts and services with this check-command, multiple
+templates can reference the same check-command.
+
+If multiple check commands do effectively the same thing and yield the same
+perfdata, all of them may be specified separated by comma. E.g.:
+
+```ini
+[ping-rta.graph]
+check_command = "ping, ping4, ping6"
+```
+
+### Template Structure: metric_filters <a id="templates-structure-metric-filters"></a>
+
+Define what metric to use and how many curves to display in the resulting graph.
+Each option's key represents the name of a curve. Its value the path to the
+metric in Icinga 2's [graphite naming schema](https://www.icinga.com/docs/icinga2/latest/doc/14-features/#current-graphite-schema).
+
+Curve names are used to map Graphite functions to metrics. (More on this below)
+However, they are fully arbitrary and have no further meaning outside template
+configurations.
+
+A curve's metric path must begin with either the macro `$host_name_template$`
+or `$service_name_template$` and is substituted with Icinga 2's prefix label.
+The rest of the path is arbitrary, but to get meaningful results use a valid
+path to one of the performance data metrics:
+
+ <prefix-label>.perfdata.<perfdata-label>.<metric>
+
+An example path which points to the metric `value` of the `rta` perfdata-label
+looks as follows:
+
+ $host_name_template$.perfdata.rta.value
+
+To dynamically render a graph for each performance data label found, define a
+macro in place for the actual perfdata-label:
+
+ $host_name_template$.perfdata.$perfdata_label$.value
+
+You can also use wildcards. To define a wildcard, please use the following syntax:
+
+ $macro:wildcard syntax here$
+
+ Some Examples:
+
+ $perfdata_label:{abc,def}$
+ $perfdata_label:{a*c,de*}$
+ $perfdata_label:{a[vbn]c,def}$
+
+> **Note:**
+>
+> The name of the macro for the perfdata-label is also arbitrary. You may as
+> well use a more descriptive name such as `$disk$` for the disk check. `$disk$`
+> is the same as `$disk:*$`.
+
+### Template Structure: urlparams <a id="templates-structure-urlparams"></a>
+
+Allows to define additional URL parameters to be passed to Graphite's render
+API.
+
+Each option represents a single parameter's name and value. A list of all
+supported parameters can be found [here](https://graphite.readthedocs.io/en/latest/render_api.html#graph-parameters).
+
+If you have used a macro for the curve's perfdata-label you may utilize it
+here as well:
+
+ title = "Disk usage on $disk$"
+
+You may also define URL parameters once for all templates
+(including the shipped ones) in the `default_url_params` section in
+`/etc/icingaweb2/modules/graphite/config.ini`:
+
+ [default_url_params]
+ yUnitSystem = "none"
+
+These may be overridden in the template itself:
+
+ yUnitSystem = "binary"
+
+### Template Structure: functions <a id="templates-structure-functions"></a>
+
+Allows to define Graphite functions which are applied to the metric of a
+specific curve on the graph.
+
+Each option's key must match a curve's name in order to apply the function
+to the curve's metric. A list of all supported functions can be found [here](https://graphite.readthedocs.io/en/latest/functions.html#functions).
+
+The metric in question can be referenced in the function call using the macro
+`$metric$` as shown in the following example:
+
+ alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')
+
+In addition you may utilize all other macros here as well:
+
+ alias(color(scale(divideSeries($metric$, $service_name_template$.perfdata.$disk$.max), 100), '#1a7dd7'), 'Used (%)')
+
+## Template Example <a id="templates-example"></a>
+
+The configuration examples used in this document are borrowed from the template
+for the `hostalive` check-command:
+
+```ini
+[hostalive-rta.graph]
+check_command = "hostalive"
+
+[hostalive-rta.metrics_filters]
+rta.value = "$host_name_template$.perfdata.rta.value"
+
+[hostalive-rta.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+min = "0"
+yUnitSystem = "none"
+
+[hostalive-rta.functions]
+rta.value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')"
+
+
+[hostalive-pl.graph]
+check_command = "hostalive"
+
+[hostalive-pl.metrics_filters]
+pl.value = "$host_name_template$.perfdata.pl.value"
+
+[hostalive-pl.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+min = "0"
+yUnitSystem = "none"
+
+[hostalive-pl.functions]
+pl.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
+```
+
+## Default Template Settings <a id="templates-default-settings"></a>
+
+Next to maintaining templates for specific commands, you can
+specify the default template settings in the [default.ini](https://github.com/Icinga/icingaweb2-module-graphite/blob/master/templates/default.ini)
+configuration file.
+
+The following example adjusts the background and foreground colors
+to setup the "dark mode" for graphs.
+
+First, copy the package provided configuration into the configuration
+path. Then add the `bgcolor` and `fgcolor` settings into the [urlparams](04-Templates.md#templates-structure-urlparams)
+sections for `default-host` and `default-service`.
+
+```
+cp /usr/share/icingaweb2/modules/graphite/templates/default.ini /etc/icingaweb2/modules/graphite/templates/default.ini
+
+vim /etc/icingaweb2/modules/graphite/templates/default.ini
+
+[default-host.urlparams]
+
+bgcolor = "black"
+fgcolor = "white"
+
+[default-service.urlparams]
+
+bgcolor = "black"
+fgcolor = "white"
+```
+
+The settings make use the `urlparams` section which adds the
+parameters to the render API.
+
+
+> **Note**
+>
+> Instead of modifying the color settings in the default template,
+> you can also change the Graphite configuration explained in
+> [this community topic](https://community.icinga.com/t/how-to-adjust-the-graphite-background-color/3172/3).
+
+
+## Further reading
+
+* [Troubleshooting](06-Troubleshooting.md)
diff --git a/doc/05-Troubleshooting.md b/doc/05-Troubleshooting.md
new file mode 100644
index 0000000..11ef711
--- /dev/null
+++ b/doc/05-Troubleshooting.md
@@ -0,0 +1,85 @@
+# <a id="Troubleshooting"></a>Troubleshooting
+
+## Graphs missing or not shown as expected
+
+If too less or too many graphs are shown for a host/service or the graphs don't
+look as expected, debugging becomes harder if there's no obvious error message
+like "Could not resolve host: example.com".
+
+In such cases the "graphs assembling debugger" may help:
+
+1. Navigate to the respective host/service as usual
+2. Add `&graph_debug=1` to the URL
+3. Inspect the log displayed under "Graphs assembling process record"
+
+### Example
+
+Example debug log for the host "icinga.com":
+
+```
++ Icinga check command: 'hostalive'
++ Obscured check command: NULL
++ Applying templates for check command 'hostalive'
+++ Applying template 'hostalive-rta'
++++ Fetched 1 metric(s) from 'https://example.com/metrics/expand?query=icinga2.icinga_com.host.hostalive.perfdata.rta.value'
++++ Excluded 0 metric(s)
++++ Combined 1 metric(s) to 1 chart(s)
+++ Applying template 'hostalive-pl'
++++ Fetched 1 metric(s) from 'https://example.com/metrics/expand?query=icinga2.icinga_com.host.hostalive.perfdata.pl.value'
++++ Excluded 0 metric(s)
++++ Combined 1 metric(s) to 1 chart(s)
++ Applying default templates, excluding previously used metrics
+++ Applying template 'default-host'
++++ Fetched 2 metric(s) from 'https://example.com/metrics/expand?query=icinga2.icinga_com.host.hostalive.perfdata.%2A.value'
++++ Excluded 2 metric(s)
++++ Combined 0 metric(s) to 0 chart(s)
+++ Not applying template 'default-service'
+```
+
+The log describes how the Icinga Web Graphite Integration assembled the
+displayed graphs (or why no graphs could be assembled). The plus signs indent
+the performed actions to visualize their hierarchy, e.g. all actions below
+`Applying templates for check command 'hostalive'` indented with more than one
+plus sign (until `Applying default templates, (...)`) are sub-actions of the
+above one.
+
+#### Details
+
+At first the host's check command is being determined. Then all templates made
+for that check command are applied. Finally, the default template is applied.
+
+For each template the available Graphite metrics are fetched and combined to
+graphs if possible. (See also [Templates](04-Templates.md).) The actual metrics
+are not shown not to make the log too large. But they can be viewed at the shown
+URLs.
+
+Example result of the first URL:
+
+```
+{"results": ["icinga2.icinga_com.host.hostalive.perfdata.rta.value"]}
+```
+
+## Special chars in host or service name
+
+Graphite cannot work with special characters. The host and service name should
+therefore only contain Latin characters. If you want to use special characters
+in host and service names, please set a `display_name` for the object.
+
+### Example
+
+```
+object Host "Only latin chars here" {
+ display_name = "Special chars are welcome"
+ ...
+}
+
+object Service "Only latin chars here" {
+ display_name = "Special chars are welcome"
+ ...
+}
+
+apply Service "Only latin chars here" {
+ display_name = "Special chars are welcome"
+ ...
+}
+```
diff --git a/doc/06-Development.md b/doc/06-Development.md
new file mode 100644
index 0000000..bfa2a57
--- /dev/null
+++ b/doc/06-Development.md
@@ -0,0 +1,15 @@
+# Development
+
+There is a CLI command for demonstrating
+graph templates (useful for developing them):
+
+```bash
+icingacli graphite icinga2 config
+```
+
+It generates Icinga 2 config based on the present graph templates.
+With this config Icinga will (also) "monitor" dummy services yielding random
+perfdata as expected by the graph templates.
+
+I. e.: If that Icinga is also writing to the Graphite that is read by your
+Icinga Web Graphite Integration, you'll get dummy graphs for all templates.
diff --git a/doc/img/service-detail-view.png b/doc/img/service-detail-view.png
new file mode 100644
index 0000000..807524b
--- /dev/null
+++ b/doc/img/service-detail-view.png
Binary files differ
diff --git a/doc/img/service-list.png b/doc/img/service-list.png
new file mode 100644
index 0000000..b234068
--- /dev/null
+++ b/doc/img/service-list.png
Binary files differ
diff --git a/library/Graphite/Graphing/Chart.php b/library/Graphite/Graphing/Chart.php
new file mode 100644
index 0000000..ded8ae8
--- /dev/null
+++ b/library/Graphite/Graphing/Chart.php
@@ -0,0 +1,385 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use Icinga\Module\Graphite\Web\Widget\GraphImage;
+use Icinga\Web\Response;
+
+class Chart
+{
+ /**
+ * Used to render the chart
+ *
+ * @var GraphiteWebClient
+ */
+ protected $graphiteWebClient;
+
+ /** @var ?string This chart's background color */
+ protected $bgcolor;
+
+ /** @var ?string This chart's foreground color */
+ protected $fgcolor;
+
+ /** @var ?string This chart's major grid line color */
+ protected $majorGridLineColor;
+
+ /** @var ?string This chart's minor grid line color */
+ protected $minorGridLineColor;
+
+ /**
+ * This chart's base
+ *
+ * @var Template
+ */
+ protected $template;
+
+ /**
+ * Target metrics by curve name
+ *
+ * @var string[]
+ */
+ protected $metrics;
+
+ /**
+ * The chart's begin
+ *
+ * @var string
+ */
+ protected $from = '-14400';
+
+ /**
+ * The chart's end
+ *
+ * @var string
+ */
+ protected $until;
+
+ /**
+ * The chart's width
+ *
+ * @var int
+ */
+ protected $width = 350;
+
+ /**
+ * The chart's height
+ *
+ * @var int
+ */
+ protected $height = 200;
+
+ /**
+ * Whether to show the chart's legend
+ *
+ * @var bool
+ */
+ protected $showLegend = true;
+
+ /**
+ * Constructor
+ *
+ * @param GraphiteWebClient $graphiteWebClient Used to render the chart
+ * @param Template $template This chart's base
+ * @param string[] $metrics Target metrics by curve name
+ */
+ public function __construct(GraphiteWebClient $graphiteWebClient, Template $template, array $metrics)
+ {
+ $this->graphiteWebClient = $graphiteWebClient;
+ $this->template = $template;
+ $this->metrics = $metrics;
+ }
+
+ /**
+ * Let Graphite Web render this chart and serve the result immediately to the user agent (via the given response)
+ *
+ * Does not return.
+ *
+ * @param Response $response
+ */
+ public function serveImage(Response $response)
+ {
+ $image = new GraphImage($this);
+
+ // Errors should occur now or not at all
+ $image->render();
+
+ $response
+ ->setHeader('Content-Type', 'image/png', true)
+ ->setHeader('Content-Disposition', 'inline; filename="graph.png"', true)
+ ->setHeader('Cache-Control', null, true)
+ ->setHeader('Expires', null, true)
+ ->setHeader('Pragma', null, true)
+ ->setBody($image)
+ ->sendResponse();
+
+ exit;
+ }
+
+ /**
+ * Extract the values of the template's metrics filters' variables from the target metrics
+ *
+ * @return string[]
+ */
+ public function getMetricVariables()
+ {
+ /** @var MacroTemplate[][] $curves */
+ $curves = $this->template->getFullCurves();
+ $variables = [];
+
+ foreach ($this->metrics as $curveName => $metric) {
+ $vars = $curves[$curveName][0]->reverseResolve($metric);
+ if ($vars !== false) {
+ $variables = array_merge($variables, $vars);
+ }
+ }
+
+ return $variables;
+ }
+
+ /**
+ * Get Graphite Web client
+ *
+ * @return GraphiteWebClient
+ */
+ public function getGraphiteWebClient()
+ {
+ return $this->graphiteWebClient;
+ }
+
+ /**
+ * Get this chart's background color
+ *
+ * @return string|null
+ */
+ public function getBackgroundColor(): ?string
+ {
+ return $this->bgcolor;
+ }
+
+ /**
+ * Set this chart's background color
+ *
+ * @param string|null $color
+ *
+ * @return $this
+ */
+ public function setBackgroundColor(?string $color): self
+ {
+ $this->bgcolor = $color;
+
+ return $this;
+ }
+
+ /**
+ * Get this chart's foreground color
+ *
+ * @return string|null
+ */
+ public function getForegroundColor(): ?string
+ {
+ return $this->fgcolor;
+ }
+
+ /**
+ * Set this chart's foreground color
+ *
+ * @param string|null $color
+ *
+ * @return $this
+ */
+ public function setForegroundColor(?string $color): self
+ {
+ $this->fgcolor = $color;
+
+ return $this;
+ }
+
+ /**
+ * Get this graph's major grid line color
+ *
+ * @return string|null
+ */
+ public function getMajorGridLineColor(): ?string
+ {
+ return $this->majorGridLineColor;
+ }
+
+ /**
+ * Set this graph's major grid line color
+ *
+ * @param string|null $color
+ *
+ * @return $this
+ */
+ public function setMajorGridLineColor(?string $color): self
+ {
+ $this->majorGridLineColor = $color;
+
+ return $this;
+ }
+
+ /**
+ * Get this graph's minor grid line color
+ *
+ * @return string|null
+ */
+ public function getMinorGridLineColor(): ?string
+ {
+ return $this->minorGridLineColor;
+ }
+
+ /**
+ * Set this graph's minor grid line color
+ *
+ * @param string|null $color
+ *
+ * @return $this
+ */
+ public function setMinorGridLineColor(?string $color): self
+ {
+ $this->minorGridLineColor = $color;
+
+ return $this;
+ }
+
+ /**
+ * Get template
+ *
+ * @return Template
+ */
+ public function getTemplate()
+ {
+ return $this->template;
+ }
+
+ /**
+ * Get metrics
+ *
+ * @return string[]
+ */
+ public function getMetrics()
+ {
+ return $this->metrics;
+ }
+
+ /**
+ * Get begin
+ *
+ * @return string
+ */
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ /**
+ * Set begin
+ *
+ * @param string $from
+ *
+ * @return $this
+ */
+ public function setFrom($from)
+ {
+ $this->from = $from;
+
+ return $this;
+ }
+
+ /**
+ * Get end
+ *
+ * @return string
+ */
+ public function getUntil()
+ {
+ return $this->until;
+ }
+
+ /**
+ * Set end
+ *
+ * @param string $until
+ *
+ * @return $this
+ */
+ public function setUntil($until)
+ {
+ $this->until = $until;
+
+ return $this;
+ }
+
+ /**
+ * Get width
+ *
+ * @return int
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * Set width
+ *
+ * @param int $width
+ *
+ * @return $this
+ */
+ public function setWidth($width)
+ {
+ $this->width = $width;
+
+ return $this;
+ }
+
+ /**
+ * Get height
+ *
+ * @return int
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * Set height
+ *
+ * @param int $height
+ *
+ * @return $this
+ */
+ public function setHeight($height)
+ {
+ $this->height = $height;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to show the chart's legend
+ *
+ * @return bool
+ */
+ public function getShowLegend()
+ {
+ return $this->showLegend;
+ }
+
+ /**
+ * Set whether to show the chart's legend
+ *
+ * @param bool $showLegend
+ *
+ * @return $this
+ */
+ public function setShowLegend($showLegend)
+ {
+ $this->showLegend = $showLegend;
+
+ return $this;
+ }
+}
diff --git a/library/Graphite/Graphing/GraphingTrait.php b/library/Graphite/Graphing/GraphingTrait.php
new file mode 100644
index 0000000..e32a52a
--- /dev/null
+++ b/library/Graphite/Graphing/GraphingTrait.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Graphite\Web\FakeSchemeRequest;
+use Icinga\Web\Url;
+
+trait GraphingTrait
+{
+ /**
+ * All loaded templates
+ *
+ * @var Templates
+ */
+ protected static $allTemplates;
+
+ /**
+ * Metrics data source
+ *
+ * @var MetricsDataSource
+ */
+ protected static $metricsDataSource;
+
+ /**
+ * Load and get all templates
+ *
+ * @return Templates
+ */
+ protected static function getAllTemplates()
+ {
+ if (static::$allTemplates === null) {
+ $allTemplates = (new Templates())->loadDir(
+ Icinga::app()
+ ->getModuleManager()
+ ->getModule('graphite')
+ ->getBaseDir() . DIRECTORY_SEPARATOR . 'templates'
+ );
+
+ $path = Config::resolvePath('modules/graphite/templates');
+ if (file_exists($path)) {
+ $allTemplates->loadDir($path);
+ }
+
+ static::$allTemplates = $allTemplates;
+ }
+
+ return static::$allTemplates;
+ }
+
+ /**
+ * Get metrics data source
+ *
+ * @return MetricsDataSource
+ *
+ * @throws ConfigurationError
+ */
+ public static function getMetricsDataSource()
+ {
+ if (static::$metricsDataSource === null) {
+ $config = Config::module('graphite');
+ $graphite = $config->getSection('graphite');
+ if (! isset($graphite->url)) {
+ throw new ConfigurationError('Missing "graphite.url" in "%s"', $config->getConfigFile());
+ }
+
+ static::$metricsDataSource = new MetricsDataSource(
+ (new GraphiteWebClient(Url::fromPath($graphite->url, [], new FakeSchemeRequest())))
+ ->setUser($graphite->user)
+ ->setPassword($graphite->password)
+ ->setInsecure($graphite->insecure)
+ );
+ }
+
+ return static::$metricsDataSource;
+ }
+}
diff --git a/library/Graphite/Graphing/GraphiteWebClient.php b/library/Graphite/Graphing/GraphiteWebClient.php
new file mode 100644
index 0000000..b06b6ce
--- /dev/null
+++ b/library/Graphite/Graphing/GraphiteWebClient.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Web\Url;
+use iplx\Http\Client;
+use iplx\Http\ClientInterface;
+use iplx\Http\Request;
+
+/**
+ * HTTP interface to Graphite Web
+ */
+class GraphiteWebClient
+{
+ /**
+ * Base URL of every Graphite Web HTTP request
+ *
+ * @var Url
+ */
+ protected $baseUrl;
+
+ /**
+ * HTTP basic auth user for every Graphite Web HTTP request
+ *
+ * @var string|null
+ */
+ protected $user;
+
+ /**
+ * The above user's password
+ *
+ * @var string|null
+ */
+ protected $password;
+
+ /**
+ * Don't verify the remote's TLS certificate
+ *
+ * @var bool
+ */
+ protected $insecure = false;
+
+ /**
+ * HTTP client
+ *
+ * @var ClientInterface
+ */
+ protected $httpClient;
+
+ /**
+ * Constructor
+ *
+ * @param Url $baseUrl Base URL of every Graphite Web HTTP request
+ */
+ public function __construct(Url $baseUrl)
+ {
+ $this->httpClient = new Client();
+
+ $this->setBaseUrl($baseUrl);
+ }
+
+ /**
+ * Send an HTTP request to the configured Graphite Web and return the response's body
+ *
+ * @param Url $url
+ * @param string $method
+ * @param string[] $headers
+ * @param string $body
+ *
+ * @return string
+ */
+ public function request(Url $url, $method = 'GET', array $headers = [], $body = null)
+ {
+ $headers['User-Agent'] = 'icingaweb2-module-graphite';
+ if ($this->user !== null) {
+ $headers['Authorization'] = 'Basic ' . base64_encode("{$this->user}:{$this->password}");
+ }
+
+ // TODO(ak): keep connections alive (TCP handshakes are a bit expensive and TLS handshakes are very expensive)
+ return (string) $this->httpClient->send(
+ new Request($method, $this->completeUrl($url)->getAbsoluteUrl(), $headers, $body),
+ ['curl' => [
+ CURLOPT_SSL_VERIFYPEER => ! $this->insecure
+ ]]
+ )->getBody();
+ }
+
+ /**
+ * Complete the given relative URL according to the base URL
+ *
+ * @param Url $url
+ *
+ * @return Url
+ */
+ public function completeUrl(Url $url)
+ {
+ $completeUrl = clone $this->baseUrl;
+ return $completeUrl
+ ->setPath(ltrim(rtrim($completeUrl->getPath(), '/') . '/' . ltrim($url->getPath(), '/'), '/'))
+ ->setParams($url->getParams());
+ }
+
+ /**
+ * Get the base URL of every Graphite Web HTTP request
+ *
+ * @return Url
+ */
+ public function getBaseUrl()
+ {
+ return $this->baseUrl;
+ }
+
+ /**
+ * Set the base URL of every Graphite Web HTTP request
+ *
+ * @param Url $baseUrl
+ *
+ * @return $this
+ */
+ public function setBaseUrl(Url $baseUrl)
+ {
+ $this->baseUrl = $baseUrl;
+
+ return $this;
+ }
+
+ /**
+ * Get the HTTP basic auth user
+ *
+ * @return null|string
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the HTTP basic auth user
+ *
+ * @param null|string $user
+ *
+ * @return $this
+ */
+ public function setUser($user)
+ {
+ $this->user = $user;
+
+ return $this;
+ }
+
+ /**
+ * Get the HTTP basic auth password
+ *
+ * @return null|string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ /**
+ * Set the HTTP basic auth password
+ *
+ * @param null|string $password
+ *
+ * @return $this
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+
+ return $this;
+ }
+
+ /**
+ * Get whether not to verify the remote's TLS certificate
+ *
+ * @return bool
+ */
+ public function getInsecure()
+ {
+ return $this->insecure;
+ }
+
+ /**
+ * Set whether not to verify the remote's TLS certificate
+ *
+ * @param bool $insecure
+ *
+ * @return $this
+ */
+ public function setInsecure($insecure = true)
+ {
+ $this->insecure = $insecure;
+
+ return $this;
+ }
+}
diff --git a/library/Graphite/Graphing/MetricsDataSource.php b/library/Graphite/Graphing/MetricsDataSource.php
new file mode 100644
index 0000000..19787da
--- /dev/null
+++ b/library/Graphite/Graphing/MetricsDataSource.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Data\Selectable;
+
+/**
+ * Provides an interface to Graphite Web's metrics list
+ */
+class MetricsDataSource implements Selectable
+{
+ /**
+ * HTTP interface to Graphite Web
+ *
+ * @var GraphiteWebClient
+ */
+ private $client;
+
+ /**
+ * Constructor
+ *
+ * @param GraphiteWebClient $client HTTP interface to Graphite Web
+ */
+ public function __construct(GraphiteWebClient $client)
+ {
+ $this->client = $client;
+ }
+
+ /**
+ * Initiate a new query
+ *
+ * @return MetricsQuery
+ */
+ public function select()
+ {
+ return new MetricsQuery($this);
+ }
+
+ /**
+ * Get the client passed to the constructor
+ *
+ * @return GraphiteWebClient
+ */
+ public function getClient()
+ {
+ return $this->client;
+ }
+}
diff --git a/library/Graphite/Graphing/MetricsQuery.php b/library/Graphite/Graphing/MetricsQuery.php
new file mode 100644
index 0000000..da05c17
--- /dev/null
+++ b/library/Graphite/Graphing/MetricsQuery.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Data\Fetchable;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filterable;
+use Icinga\Data\Queryable;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Module\Graphite\GraphiteUtil;
+use Icinga\Module\Graphite\Util\IcingadbUtils;
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
+use Icinga\Module\Icingadb\Compat\UrlMigrator;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Monitoring\Object\Macro;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Util\Json;
+use Icinga\Web\Url;
+use InvalidArgumentException;
+use ipl\Orm\Model;
+use ipl\Stdlib\Filter as IplFilter;
+
+/**
+ * Queries a {@link MetricsDataSource}
+ */
+class MetricsQuery implements Queryable, Filterable, Fetchable
+{
+ /**
+ * @var MetricsDataSource
+ */
+ protected $dataSource;
+
+ /**
+ * The base metrics pattern
+ *
+ * @var MacroTemplate
+ */
+ protected $base;
+
+ /**
+ * Extension of {@link base}
+ *
+ * @var string[]
+ */
+ protected $filter = [];
+
+ /**
+ * The object to render the graphs for
+ *
+ * @var MonitoredObject|Model
+ */
+ protected $object;
+
+ /**
+ * Constructor
+ *
+ * @param MetricsDataSource $dataSource
+ */
+ public function __construct(MetricsDataSource $dataSource)
+ {
+ $this->dataSource = $dataSource;
+ }
+
+ public function from($target, array $fields = null)
+ {
+ if ($fields !== null) {
+ throw new InvalidArgumentException('Fields are not applicable to this kind of query');
+ }
+
+ try {
+ $this->base = $target instanceof MacroTemplate ? $target : new MacroTemplate((string) $target);
+ } catch (InvalidArgumentException $e) {
+ throw new InvalidArgumentException('Bad target', $e);
+ }
+
+ return $this;
+ }
+
+ public function applyFilter(Filter $filter)
+ {
+ throw new NotImplementedError(__METHOD__);
+ }
+
+ public function setFilter(Filter $filter)
+ {
+ throw new NotImplementedError(__METHOD__);
+ }
+
+ public function getFilter()
+ {
+ throw new NotImplementedError(__METHOD__);
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ throw new NotImplementedError(__METHOD__);
+ }
+
+ public function where($condition, $value = null)
+ {
+ $this->filter[$condition] = $this->escapeMetricStep($value);
+
+ return $this;
+ }
+
+ public function fetchAll()
+ {
+ $result = [];
+ foreach ($this->fetchColumn() as $metric) {
+ $result[] = (object) ['metric' => $metric];
+ }
+
+ return $result;
+ }
+
+ public function fetchRow()
+ {
+ $result = $this->fetchColumn();
+ return empty($result) ? false : (object) ['metric' => $result[0]];
+ }
+
+ public function fetchColumn()
+ {
+ $filter = [];
+ foreach ($this->base->getMacros() as $macro) {
+ if (isset($this->filter[$macro])) {
+ $filter[$macro] = $this->filter[$macro];
+ continue;
+ }
+
+ if (strpos($macro, '.') === false) {
+ continue;
+ }
+
+ $workaroundMacro = str_replace('.', '_', $macro);
+ if ($this->object instanceof Model) {
+ // icingadb macro
+ $tranformFilter = UrlMigrator::transformFilter(
+ IplFilter::equal($workaroundMacro, ''),
+ $this->object instanceof Host ? 'hosts' : 'services'
+ );
+
+ if ($tranformFilter === false) {
+ continue;
+ }
+
+ $migratedMacro = $tranformFilter->getColumn();
+
+ if ($migratedMacro === $workaroundMacro) {
+ $workaroundMacro = $macro;
+ } else {
+ $workaroundMacro = $migratedMacro;
+ }
+
+ $icingadbMacros = IcingadbUtils::getInstance();
+ $result = $icingadbMacros->resolveMacro($workaroundMacro, $this->object);
+ } else {
+ if ($workaroundMacro === 'service_name') {
+ $workaroundMacro = 'service_description';
+ }
+
+ $result = Macro::resolveMacro($workaroundMacro, $this->object);
+ }
+
+ if ($result !== $workaroundMacro) {
+ $filter[$macro] = $this->escapeMetricStep($result);
+ }
+ }
+
+ $client = $this->dataSource->getClient();
+ $url = Url::fromPath('metrics/expand', [
+ 'query' => $this->base->resolve($filter, '*')
+ ]);
+ $res = Json::decode($client->request($url));
+ natsort($res->results);
+
+ IPT::recordf('Fetched %s metric(s) from %s', count($res->results), (string) $client->completeUrl($url));
+
+ return array_values($res->results);
+ }
+
+ public function fetchOne()
+ {
+ $result = $this->fetchColumn();
+ return empty($result) ? false : $result[0];
+ }
+
+ public function fetchPairs()
+ {
+ throw new NotImplementedError(__METHOD__);
+ }
+
+ /**
+ * Set the object to render the graphs for
+ *
+ * @param MonitoredObject|Model $object
+ *
+ * @return $this
+ */
+ public function setObject($object)
+ {
+ $this->object = $object;
+
+ return $this;
+ }
+
+ /**
+ * Escapes a string for usage in a Graphite metric path between two dots
+ *
+ * @param string $step
+ *
+ * @return string
+ */
+ protected function escapeMetricStep($step)
+ {
+ return preg_replace('/[^a-zA-Z0-9\*\-:^[\]$#%\']/', '_', $step);
+ }
+}
diff --git a/library/Graphite/Graphing/Template.php b/library/Graphite/Graphing/Template.php
new file mode 100644
index 0000000..a030fb7
--- /dev/null
+++ b/library/Graphite/Graphing/Template.php
@@ -0,0 +1,364 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Application\Config;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use InvalidArgumentException;
+use ipl\Orm\Model;
+
+class Template
+{
+ /**
+ * The configured icinga.graphite_writer_host_name_template
+ *
+ * @var MacroTemplate
+ */
+ protected static $hostNameTemplate;
+
+ /**
+ * The configured icinga.graphite_writer_service_name_template
+ *
+ * @var MacroTemplate
+ */
+ protected static $serviceNameTemplate;
+
+ /**
+ * All curves to show in a chart by name with Graphite Web metric filters and Graphite functions
+ *
+ * [$curve => [$metricFilter, $function], ...]
+ *
+ * @var MacroTemplate[][]
+ */
+ protected $curves = [];
+
+ /**
+ * All curves to show in a chart by name with full Graphite Web metric filters and Graphite functions
+ *
+ * [$curve => [$metricFilter, $function], ...]
+ *
+ * @var MacroTemplate[][]
+ */
+ protected $fullCurves;
+
+ /**
+ * Additional URL parameters for rendering via Graphite Web
+ *
+ * [$key => $value, ...]
+ *
+ * @var MacroTemplate[]
+ */
+ protected $urlParams = [];
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ }
+
+ /**
+ * Get all charts based on this template and applicable to the metrics
+ * from the given data source restricted by the given filter
+ *
+ * @param MetricsDataSource $dataSource
+ * @param MonitoredObject|Model $object The object to render the graphs for
+ * @param string[] $filter
+ * @param MacroTemplate[] $excludeMetrics
+ *
+ * @return Chart[]
+ */
+ public function getCharts(
+ MetricsDataSource $dataSource,
+ $object,
+ array $filter,
+ array &$excludeMetrics = []
+ ) {
+ $metrics = [];
+ $metricsUsed = 0;
+ $metricsExcluded = 0;
+
+ foreach ($this->getFullCurves() as $curveName => $curve) {
+ $fullMetricTemplate = $curve[0];
+
+ $query = $dataSource->select()->setObject($object)->from($fullMetricTemplate);
+
+ foreach ($filter as $key => $value) {
+ $query->where($key, $value);
+ }
+
+ foreach ($query->fetchColumn() as $metric) {
+ foreach ($excludeMetrics as $excludeMetric) {
+ if ($excludeMetric->reverseResolve($metric) !== false) {
+ ++$metricsExcluded;
+ continue 2;
+ }
+ }
+
+ $vars = $curve[0]->reverseResolve($metric);
+ if ($vars !== false) {
+ $metrics[$curveName][$metric] = $vars;
+ ++$metricsUsed;
+ }
+ }
+ }
+
+ switch (count($metrics)) {
+ case 0:
+ $metricsCombinations = [];
+ break;
+
+ case 1:
+ $metricsCombinations = [];
+
+ foreach ($metrics as $curveName => & $curveMetrics) {
+ foreach ($curveMetrics as $metric => & $_) {
+ $metricsCombinations[] = [$curveName => $metric];
+ }
+ unset($_);
+ }
+ unset($curveMetrics);
+
+ break;
+
+ default:
+ $possibleCombinations = [];
+ $handledCurves = [];
+ foreach ($metrics as $curveName1 => & $metrics1) {
+ $handledCurves[$curveName1] = true;
+
+ foreach ($metrics as $curveName2 => & $metrics2) {
+ if (! isset($handledCurves[$curveName2])) {
+ foreach ($metrics1 as $metric1 => & $vars1) {
+ foreach ($metrics2 as $metric2 => & $vars2) {
+ if (
+ count(array_intersect_assoc($vars1, $vars2))
+ === count(array_intersect_key($vars1, $vars2))
+ ) {
+ $possibleCombinations[$curveName1][$curveName2][$metric1][$metric2] = true;
+ }
+ }
+ unset($vars2);
+ }
+ unset($vars1);
+ }
+ }
+ unset($metrics2);
+ }
+ unset($metrics1);
+
+ $metricsCombinations = [];
+ $this->combineMetrics($metrics, $possibleCombinations, $metricsCombinations);
+ }
+
+ $charts = [];
+ foreach ($metricsCombinations as $metricsCombination) {
+ $charts[] = new Chart($dataSource->getClient(), $this, $metricsCombination);
+ }
+
+ IPT::recordf('Excluded %s metric(s)', $metricsExcluded);
+ IPT::recordf('Combined %s metric(s) to %s chart(s)', $metricsUsed, count($charts));
+
+ return $charts;
+ }
+
+ /**
+ * Fill the given metrics combinations from the given metrics as restricted by the given possible combinations
+ *
+ * @param string[][][] $metrics
+ * @param bool[][][][] $possibleCombinations
+ * @param string[][] $metricsCombinations
+ * @param string[] $currentCombination
+ */
+ protected function combineMetrics(
+ array &$metrics,
+ array &$possibleCombinations,
+ array &$metricsCombinations,
+ array $currentCombination = []
+ ) {
+ if (empty($currentCombination)) {
+ foreach ($metrics as $curveName => & $curveMetrics) {
+ foreach ($curveMetrics as $metric => & $_) {
+ $this->combineMetrics(
+ $metrics,
+ $possibleCombinations,
+ $metricsCombinations,
+ [$curveName => $metric]
+ );
+ }
+ unset($_);
+
+ break;
+ }
+ unset($curveMetrics);
+ } elseif (count($currentCombination) === count($metrics)) {
+ $metricsCombinations[] = $currentCombination;
+ } else {
+ foreach ($metrics as $nextCurveName => & $_) {
+ if (! isset($currentCombination[$nextCurveName])) {
+ break;
+ }
+ }
+ unset($_);
+
+ $allowedNextCurveMetricsPerCurrentCurveName = [];
+ foreach ($currentCombination as $currentCurveName => $currentCurveMetric) {
+ $allowedNextCurveMetricsPerCurrentCurveName[$currentCurveName]
+ = $possibleCombinations[$currentCurveName][$nextCurveName][$currentCurveMetric];
+ }
+
+ $allowedNextCurveMetrics = $allowedNextCurveMetricsPerCurrentCurveName[$currentCurveName];
+ unset($allowedNextCurveMetricsPerCurrentCurveName[$currentCurveName]);
+
+ foreach ($allowedNextCurveMetricsPerCurrentCurveName as & $allowedMetrics) {
+ $allowedNextCurveMetrics = array_intersect_key($allowedNextCurveMetrics, $allowedMetrics);
+ }
+ unset($allowedMetrics);
+
+ foreach ($allowedNextCurveMetrics as $allowedNextCurveMetric => $_) {
+ $nextCombination = $currentCombination;
+ $nextCombination[$nextCurveName] = $allowedNextCurveMetric;
+
+ $this->combineMetrics($metrics, $possibleCombinations, $metricsCombinations, $nextCombination);
+ }
+ }
+ }
+
+ /**
+ * Get curves to show in a chart by name with Graphite Web metric filters and Graphite functions
+ *
+ * @return MacroTemplate[][]
+ */
+ public function getCurves()
+ {
+ return $this->curves;
+ }
+
+ /**
+ * Get curves to show in a chart by name with full Graphite Web metric filters and Graphite functions
+ *
+ * @return MacroTemplate[][]
+ */
+ public function getFullCurves()
+ {
+ if ($this->fullCurves === null) {
+ $curves = $this->curves;
+
+ foreach ($curves as &$curve) {
+ $curve[0] = new MacroTemplate($curve[0]->resolve([
+ 'host_name_template' => static::getHostNameTemplate(),
+ 'service_name_template' => static::getServiceNameTemplate(),
+ '' => '$$'
+ ]));
+ }
+ unset($curve);
+
+ $this->fullCurves = $curves;
+ }
+
+ return $this->fullCurves;
+ }
+
+ /**
+ * Set curves to show in a chart by name with Graphite Web metric filters and Graphite functions
+ *
+ * @param MacroTemplate[][] $curves
+ *
+ * @return $this
+ */
+ public function setCurves(array $curves)
+ {
+ $this->curves = $curves;
+
+ return $this;
+ }
+
+ /**
+ * Get additional URL parameters for Graphite Web
+ *
+ * @return MacroTemplate[]
+ */
+ public function getUrlParams()
+ {
+ return $this->urlParams;
+ }
+
+ /**
+ * Set additional URL parameters for Graphite Web
+ *
+ * @param MacroTemplate[] $urlParams
+ *
+ * @return $this
+ */
+ public function setUrlParams(array $urlParams)
+ {
+ $this->urlParams = $urlParams;
+
+ return $this;
+ }
+
+ /**
+ * Get {@link hostNameTemplate}
+ *
+ * @return MacroTemplate
+ *
+ * @throws ConfigurationError If the configuration is invalid
+ */
+ protected static function getHostNameTemplate()
+ {
+ if (static::$hostNameTemplate === null) {
+ $config = Config::module('graphite');
+ $template = $config->get(
+ 'icinga',
+ 'graphite_writer_host_name_template',
+ 'icinga2.$host.name$.host.$host.check_command$'
+ );
+
+ try {
+ static::$hostNameTemplate = new MacroTemplate($template);
+ } catch (InvalidArgumentException $e) {
+ throw new ConfigurationError(
+ 'Bad icinga.graphite_writer_host_name_template in "%s": %s',
+ $config->getConfigFile(),
+ $e->getMessage()
+ );
+ }
+ }
+
+ return static::$hostNameTemplate;
+ }
+
+ /**
+ * Get {@link serviceNameTemplate}
+ *
+ * @return MacroTemplate
+ *
+ * @throws ConfigurationError If the configuration is invalid
+ */
+ protected static function getServiceNameTemplate()
+ {
+ if (static::$serviceNameTemplate === null) {
+ $config = Config::module('graphite');
+ $template = $config->get(
+ 'icinga',
+ 'graphite_writer_service_name_template',
+ 'icinga2.$host.name$.services.$service.name$.$service.check_command$'
+ );
+
+ try {
+ static::$serviceNameTemplate = new MacroTemplate($template);
+ } catch (InvalidArgumentException $e) {
+ throw new ConfigurationError(
+ 'Bad icinga.graphite_writer_service_name_template in "%s": %s',
+ $config->getConfigFile(),
+ $e->getMessage()
+ );
+ }
+ }
+
+ return static::$serviceNameTemplate;
+ }
+}
diff --git a/library/Graphite/Graphing/Templates.php b/library/Graphite/Graphing/Templates.php
new file mode 100644
index 0000000..0765e46
--- /dev/null
+++ b/library/Graphite/Graphing/Templates.php
@@ -0,0 +1,321 @@
+<?php
+
+namespace Icinga\Module\Graphite\Graphing;
+
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use Icinga\Web\UrlParams;
+use InvalidArgumentException;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use SplFileInfo;
+
+/**
+ * Templates collection
+ */
+class Templates
+{
+ /**
+ * All templates by their check command and name
+ *
+ * @var Template[][]
+ */
+ protected $templates = [];
+
+ /**
+ * All default templates by their name
+ *
+ * @var Template[]
+ */
+ protected $defaultTemplates = [];
+
+ /**
+ * Default URL params for all templates
+ *
+ * @var string[]
+ */
+ protected $defaultUrlParams = [];
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $config = Config::module('graphite');
+
+ foreach ($config->getSection('default_url_params') as $param => $value) {
+ try {
+ $this->defaultUrlParams[$param] = new MacroTemplate($value);
+ } catch (InvalidArgumentException $e) {
+ throw new ConfigurationError(
+ 'Invalid URL parameter "%s" ("%s") in file "%s"',
+ $param,
+ $value,
+ $config->getConfigFile(),
+ $e
+ );
+ }
+ }
+ }
+
+ /**
+ * Load templates as configured inside the given directory
+ *
+ * @param string $path
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError If the configuration is invalid
+ */
+ public function loadDir($path)
+ {
+ foreach (
+ new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(
+ $path,
+ RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_FILEINFO
+ | RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS
+ ),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ ) as $filepath => $fileinfo
+ ) {
+ /** @var SplFileInfo $fileinfo */
+
+ if ($fileinfo->isFile() && preg_match('/\A[^.].*\.ini\z/si', $fileinfo->getFilename())) {
+ $this->loadIni($filepath);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Load templates as configured in the given INI file
+ *
+ * @param string $path
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError If the configuration is invalid
+ */
+ public function loadIni($path)
+ {
+ /** @var string[][][] $templates */
+ $templates = [];
+
+ foreach (Config::fromIni($path) as $section => $options) {
+ /** @var ConfigObject $options */
+
+ $matches = [];
+ if (! preg_match('/\A(.+)\.(graph|metrics_filters|urlparams|functions)\z/', $section, $matches)) {
+ throw new ConfigurationError('Bad section name "%s" in file "%s"', $section, $path);
+ }
+
+ $templates[$matches[1]][$matches[2]] = $options->toArray();
+ }
+
+ $checkCommands = [];
+
+ foreach ($templates as $templateName => $template) {
+ $checkCommands[$templateName] = isset($template['graph']['check_command'])
+ ? array_unique(preg_split('/\s*,\s*/', $template['graph']['check_command'], -1, PREG_SPLIT_NO_EMPTY))
+ : [];
+ unset($template['graph']['check_command']);
+
+ if (isset($template['graph'])) {
+ switch (count($template['graph'])) {
+ case 0:
+ break;
+
+ case 1:
+ throw new ConfigurationError(
+ 'Bad option for template "%s" in file "%s": "graph.%s"',
+ $templateName,
+ $path,
+ array_keys($template['graph'])[0]
+ );
+
+ default:
+ $standalone = array_keys($template['graph']);
+ sort($standalone);
+
+ throw new ConfigurationError(
+ 'Bad options for template "%s" in file "%s": %s',
+ $templateName,
+ $path,
+ implode(', ', array_map(
+ function ($option) {
+ return "\"graph.$option\"";
+ },
+ $standalone
+ ))
+ );
+ }
+ }
+
+ /** @var MacroTemplate[][] $curves */
+ $curves = [];
+
+ if (isset($template['metrics_filters'])) {
+ foreach ($template['metrics_filters'] as $curve => $metricsFilter) {
+ try {
+ $curves[$curve][0] = new MacroTemplate($metricsFilter);
+ } catch (InvalidArgumentException $e) {
+ throw new ConfigurationError(
+ 'Bad metrics filter "%s" for curve "%s" of template "%s" in file "%s": %s',
+ $metricsFilter,
+ $curve,
+ $templateName,
+ $path,
+ $e->getMessage()
+ );
+ }
+
+ if (
+ count(array_intersect(
+ $curves[$curve][0]->getMacros(),
+ ['host_name_template', 'service_name_template']
+ )) !== 1
+ ) {
+ throw new ConfigurationError(
+ 'Bad metrics filter "%s" for curve "%s" of template "%s" in file "%s": must include'
+ . ' either the macro $host_name_template$ or $service_name_template$, but not both',
+ $metricsFilter,
+ $curve,
+ $templateName,
+ $path
+ );
+ }
+
+ if (isset($template['functions'][$curve])) {
+ try {
+ $curves[$curve][1] = new MacroTemplate($template['functions'][$curve]);
+ } catch (InvalidArgumentException $e) {
+ throw new ConfigurationError(
+ 'Bad function "%s" for curve "%s" of template "%s" in file "%s": %s',
+ $template['functions'][$curve],
+ $curve,
+ $templateName,
+ $path,
+ $e->getMessage()
+ );
+ }
+
+ unset($template['functions'][$curve]);
+ } else {
+ $curves[$curve][1] = new MacroTemplate('$metric$');
+ }
+ }
+ }
+
+ if (isset($template['functions'])) {
+ switch (count($template['functions'])) {
+ case 0:
+ break;
+
+ case 1:
+ throw new ConfigurationError(
+ 'Metrics filter for curve "%s" of template "%s" in file "%s" missing',
+ array_keys($template['functions'])[0],
+ $templateName,
+ $path
+ );
+
+ default:
+ $standalone = array_keys($template['functions']);
+ sort($standalone);
+
+ throw new ConfigurationError(
+ 'Metrics filter for curves of template "%s" in file "%s" missing: "%s"',
+ $templateName,
+ $path,
+ implode('", "', $standalone)
+ );
+ }
+ }
+
+ $urlParams = $this->defaultUrlParams;
+
+ if (isset($template['urlparams'])) {
+ foreach ($template['urlparams'] as $key => $value) {
+ try {
+ $urlParams[$key] = new MacroTemplate($value);
+ } catch (InvalidArgumentException $e) {
+ throw new ConfigurationError(
+ 'Invalid URL parameter "%s" ("%s") for template "%s" in file "%s": %s',
+ $key,
+ $value,
+ $templateName,
+ $path,
+ $e->getMessage()
+ );
+ }
+ }
+ }
+
+ $templates[$templateName] = empty($curves) ? null : (new Template())
+ ->setCurves($curves)
+ ->setUrlParams($urlParams);
+ }
+
+ foreach ($templates as $templateName => $template) {
+ if ($template === null) {
+ if (empty($checkCommands[$templateName])) {
+ unset($this->defaultTemplates[$templateName]);
+ } else {
+ foreach ($checkCommands[$templateName] as $checkCommand) {
+ unset($this->templates[$checkCommand][$templateName]);
+
+ if (empty($this->templates[$checkCommand])) {
+ unset($this->templates[$checkCommand]);
+ }
+ }
+ }
+ } else {
+ if (empty($checkCommands[$templateName])) {
+ $this->defaultTemplates[$templateName] = $template;
+ } else {
+ foreach ($checkCommands[$templateName] as $checkCommand) {
+ $this->templates[$checkCommand][$templateName] = $template;
+ }
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get all loaded templates for the given check command by their names
+ *
+ * @param string $checkCommand
+ *
+ * @return Template[]
+ */
+ public function getTemplates($checkCommand)
+ {
+ return isset($this->templates[$checkCommand]) ? $this->templates[$checkCommand] : [];
+ }
+
+ /**
+ * Get all loaded templates for all check commands
+ *
+ * @return Template[][]
+ */
+ public function getAllTemplates()
+ {
+ return $this->templates;
+ }
+
+ /**
+ * Get all loaded default templates by their names
+ *
+ * @return Template[]
+ */
+ public function getDefaultTemplates()
+ {
+ return $this->defaultTemplates;
+ }
+}
diff --git a/library/Graphite/ProvidedHook/Icingadb/HostDetailExtension.php b/library/Graphite/ProvidedHook/Icingadb/HostDetailExtension.php
new file mode 100644
index 0000000..31e4e6c
--- /dev/null
+++ b/library/Graphite/ProvidedHook/Icingadb/HostDetailExtension.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Graphite\ProvidedHook\Icingadb;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Icingadb\Hook\HostDetailExtensionHook;
+use Icinga\Module\Icingadb\Model\Host;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Html\ValidHtml;
+
+class HostDetailExtension extends HostDetailExtensionHook
+{
+ use TimeRangePickerTrait;
+
+ public function getHtmlForObject(Host $host): ValidHtml
+ {
+ if (Icinga::app()->getRequest()->getUrl()->getParam('graph_debug')) {
+ IPT::enable();
+ }
+
+ $graphs = (string) Graphs::forIcingadbObject($host)
+ ->setWidth(440)
+ ->setHeight(220)
+ ->setClasses(['object-detail-view'])
+ ->setPreloadDummy()
+ ->setShowNoGraphsFound(false)
+ ->handleRequest();
+
+ if (! empty($graphs)) {
+ $this->handleTimeRangePickerRequest();
+
+ $header = Html::tag('h2', [], 'Graphs');
+ $timepicker = HtmlString::create($this->renderTimeRangePicker(Icinga::app()->getViewRenderer()->view));
+ $graphColorRegistry = Html::tag('div', ['class' => 'graphite-graph-color-registry']);
+ $graphs = HtmlString::create($graphs);
+
+ return HtmlString::create($header . $timepicker . $graphColorRegistry . $graphs);
+ }
+
+ return HtmlString::create('');
+ }
+}
diff --git a/library/Graphite/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Graphite/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..8f0f38e
--- /dev/null
+++ b/library/Graphite/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Graphite\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+}
diff --git a/library/Graphite/ProvidedHook/Icingadb/ServiceDetailExtension.php b/library/Graphite/ProvidedHook/Icingadb/ServiceDetailExtension.php
new file mode 100644
index 0000000..63c2b79
--- /dev/null
+++ b/library/Graphite/ProvidedHook/Icingadb/ServiceDetailExtension.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Icinga\Module\Graphite\ProvidedHook\Icingadb;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Icingadb\Hook\ServiceDetailExtensionHook;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Html\ValidHtml;
+
+class ServiceDetailExtension extends ServiceDetailExtensionHook
+{
+ use TimeRangePickerTrait;
+
+ public function getHtmlForObject(Service $service): ValidHtml
+ {
+ if (Icinga::app()->getRequest()->getUrl()->getParam('graph_debug')) {
+ IPT::enable();
+ }
+
+ $graphs = (string) Graphs::forIcingadbObject($service)
+ ->setWidth(440)
+ ->setHeight(220)
+ ->setClasses(['object-detail-view'])
+ ->setPreloadDummy()
+ ->setShowNoGraphsFound(false)
+ ->handleRequest();
+
+ if (! empty($graphs)) {
+ $this->handleTimeRangePickerRequest();
+
+ $header = Html::tag('h2', [], 'Graphs');
+ $timepicker = HtmlString::create($this->renderTimeRangePicker(Icinga::app()->getViewRenderer()->view));
+ $graphColorRegistry = Html::tag('div', ['class' => 'graphite-graph-color-registry']);
+ $graphs = HtmlString::create($graphs);
+
+ return HtmlString::create($header . $timepicker . $graphColorRegistry . $graphs);
+ }
+
+ return HtmlString::create('');
+ }
+}
diff --git a/library/Graphite/ProvidedHook/Monitoring/DetailviewExtension.php b/library/Graphite/ProvidedHook/Monitoring/DetailviewExtension.php
new file mode 100644
index 0000000..d6a4673
--- /dev/null
+++ b/library/Graphite/ProvidedHook/Monitoring/DetailviewExtension.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Graphite\ProvidedHook\Monitoring;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
+use Icinga\Module\Graphite\Web\Controller\TimeRangePickerTrait;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+class DetailviewExtension extends DetailviewExtensionHook
+{
+ use TimeRangePickerTrait;
+
+ public function getHtmlForObject(MonitoredObject $object)
+ {
+ if (Icinga::app()->getRequest()->getUrl()->getParam('graph_debug')) {
+ IPT::enable();
+ }
+
+ $graphs = (string) Graphs::forMonitoredObject($object)
+ ->setWidth(440)
+ ->setHeight(220)
+ ->setClasses(['object-detail-view'])
+ ->setPreloadDummy()
+ ->setShowNoGraphsFound(false)
+ ->handleRequest();
+
+ if ($graphs !== '') {
+ $this->handleTimeRangePickerRequest();
+ return '<h2>' . mt('graphite', 'Graphs') . '</h2>'
+ . $this->renderTimeRangePicker($this->getView())
+ . '<div class="graphite-graph-color-registry"></div>'
+ . $graphs;
+ }
+
+ return '';
+ }
+}
diff --git a/library/Graphite/Util/IcingadbUtils.php b/library/Graphite/Util/IcingadbUtils.php
new file mode 100644
index 0000000..43334e5
--- /dev/null
+++ b/library/Graphite/Util/IcingadbUtils.php
@@ -0,0 +1,49 @@
+<?php
+
+/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Graphite\Util;
+
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Common\Macros;
+
+/**
+* Class for initialising icingadb utils
+ */
+class IcingadbUtils
+{
+ use Macros;
+ use Database;
+ use Auth;
+
+ protected static $instance;
+
+ /**
+ * @see getInstance()
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Get the IcingadbUtils instance
+ *
+ * @return IcingadbUtils
+ */
+ public static function getInstance(): IcingadbUtils
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+
+ return static::$instance;
+ }
+
+ /**
+ * Prevent the instance from being cloned (which would create a second instance of it)
+ */
+ private function __clone()
+ {
+ }
+}
diff --git a/library/Graphite/Util/InternalProcessTracker.php b/library/Graphite/Util/InternalProcessTracker.php
new file mode 100644
index 0000000..f7f2df6
--- /dev/null
+++ b/library/Graphite/Util/InternalProcessTracker.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Icinga\Module\Graphite\Util;
+
+use Icinga\Authentication\Auth;
+use Icinga\Security\SecurityException;
+
+/**
+ * A record about what happened during a specific action
+ */
+class InternalProcessTracker
+{
+ /**
+ * Whether to record anything
+ *
+ * @var bool
+ */
+ private static $enabled = false;
+
+ /**
+ * How many '+'es to prepend to each new record
+ *
+ * @var int
+ */
+ private static $indentation = 1;
+
+ /**
+ * The recorded happenings
+ *
+ * @var string[]
+ */
+ private static $records = [];
+
+ /**
+ * Get whether recording is enabled
+ *
+ * @return bool
+ */
+ public static function enabled()
+ {
+ return self::$enabled;
+ }
+
+ /**
+ * Enable recording
+ *
+ * @throws SecurityException
+ */
+ public static function enable()
+ {
+ if (! Auth::getInstance()->hasPermission('graphite/debug')) {
+ throw new SecurityException('No permission for graphite/debug');
+ }
+
+ self::$enabled = true;
+ }
+
+ /**
+ * Introduce a "sub-process"
+ */
+ public static function indent()
+ {
+ if (self::$enabled) {
+ ++self::$indentation;
+ }
+ }
+
+ /**
+ * Record a happening
+ *
+ * Behaves like {@link sprintf()} if additional arguments given, but {@link var_export()}s the arguments first
+ * (so always use %s instead of e.g. %d).
+ *
+ * @param string $format
+ */
+ public static function recordf($format)
+ {
+ if (self::$enabled) {
+ if (func_num_args() > 1) {
+ $args = [];
+ foreach (array_slice(func_get_args(), 1) as $arg) {
+ $args[] = var_export($arg, true);
+ }
+
+ $format = vsprintf($format, $args);
+ }
+
+ self::$records[] = str_repeat('+', self::$indentation) . " $format";
+ }
+ }
+
+ /**
+ * Terminate a "sub-process"
+ */
+ public static function unindent()
+ {
+ if (self::$enabled) {
+ --self::$indentation;
+ }
+ }
+
+ /**
+ * Dump everything recorded as plain text
+ *
+ * @return string
+ */
+ public static function dump()
+ {
+ return implode("\n", self::$records);
+ }
+
+ /**
+ * Reset records
+ */
+ public static function clear()
+ {
+ if (self::$enabled) {
+ self::$indentation = 1;
+ self::$records = [];
+ }
+ }
+
+ final private function __construct()
+ {
+ }
+}
diff --git a/library/Graphite/Util/MacroTemplate.php b/library/Graphite/Util/MacroTemplate.php
new file mode 100644
index 0000000..23a171a
--- /dev/null
+++ b/library/Graphite/Util/MacroTemplate.php
@@ -0,0 +1,239 @@
+<?php
+
+namespace Icinga\Module\Graphite\Util;
+
+use InvalidArgumentException;
+
+/**
+ * A macro-based template for strings
+ */
+class MacroTemplate
+{
+ /**
+ * Macros' start and end character
+ *
+ * @var string
+ */
+ protected $macroCharacter;
+
+ /**
+ * The parsed template
+ *
+ * @var string[]
+ */
+ protected $template;
+
+ /**
+ * Regex for reverse resolving patterns
+ *
+ * @var string
+ */
+ protected $reverseResolvePattern;
+
+ /**
+ * Wildcards
+ *
+ * @var string[]
+ */
+ protected $wildCards;
+
+ /**
+ * The original raw template
+ *
+ * @var string
+ */
+ protected $orgTemplate;
+
+ /**
+ * Constructor
+ *
+ * @param string $template The raw template
+ * @param string $macroCharacter Macros' start and end character
+ */
+ public function __construct($template, $macroCharacter = '$')
+ {
+ $this->orgTemplate = $template;
+ $this->macroCharacter = $macroCharacter;
+ $this->template = explode($macroCharacter, $template);
+ foreach ($this->template as $key => $value) {
+ if (preg_match('/([^:]+):(.+)/', $value, $match)) {
+ $wildCardKey = $match[1];
+ $this->template[$key] = $wildCardKey;
+ $this->wildCards[$wildCardKey] = $match[2];
+ }
+ }
+
+ if (! (count($this->template) % 2)) {
+ throw new InvalidArgumentException(
+ 'template contains odd number of ' . var_export($macroCharacter, true)
+ . 's: ' . var_export($template, true)
+ );
+ }
+ }
+
+ /**
+ * Return a string based on this template with the macros resolved from the given variables
+ *
+ * @param string[] $variables
+ * @param string $default The default value for missing variables.
+ * By default the macro just isn't replaced.
+ *
+ * @return string
+ */
+ public function resolve(array $variables, $default = null)
+ {
+ $macro = false;
+ $result = []; // kind of string builder
+
+ foreach ($this->template as $part) {
+ if ($macro) {
+ if (isset($variables[$part])) {
+ $result[] = $variables[$part];
+ } elseif ($part === '') {
+ $result[] = $this->macroCharacter;
+ } elseif ($default === null) {
+ $result[] = $this->macroCharacter;
+ $result[] = $part;
+ // add wildcards to result before they are
+ // overwritten from Template::getFullCurves()
+ if (isset($this->wildCards[$part])) {
+ $result[] = ':' . $this->wildCards[$part];
+ }
+
+ $result[] = $this->macroCharacter;
+ } else {
+ if (isset($this->wildCards[$part])) {
+ $result[] = $this->wildCards[$part];
+ } else {
+ $result[] = $default;
+ }
+ }
+ } else {
+ $result[] = $part;
+ }
+
+ $macro = ! $macro;
+ }
+
+ return implode($result);
+ }
+
+ /**
+ * Try to reverse-resolve the given string
+ *
+ * @param string $resolved A result of {@link resolve()}
+ *
+ * @return string[]|false Variables as passed to {@link resolve()} if successful
+ */
+ public function reverseResolve($resolved)
+ {
+ $matches = [];
+ if (! preg_match($this->getReverseResolvePattern(), $resolved, $matches)) {
+ return false;
+ }
+
+ $macro = false;
+ $macros = [];
+ $currentCapturedSubPatternIndex = 0;
+
+ foreach ($this->template as $part) {
+ if ($macro && ! isset($macros[$part])) {
+ $macros[$part] = ++$currentCapturedSubPatternIndex;
+ }
+
+ $macro = ! $macro;
+ }
+
+ $macros = array_flip($macros);
+
+ $result = [];
+ foreach ($matches as $index => $match) {
+ if ($index > 0) {
+ $result[$macros[$index]] = $match;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return the raw template string this instance was constructed from
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->orgTemplate;
+ }
+
+ /**
+ * Return the macros of this template
+ *
+ * @return string[]
+ */
+ public function getMacros()
+ {
+ $macro = false;
+ $macros = [];
+
+ foreach ($this->template as $part) {
+ if ($macro) {
+ $macros[$part] = null;
+ }
+
+ $macro = ! $macro;
+ }
+
+ unset($macros['']);
+
+ return array_keys($macros);
+ }
+
+ /**
+ * Get macros' start and end character
+ *
+ * @return string
+ */
+ public function getMacroCharacter()
+ {
+ return $this->macroCharacter;
+ }
+
+ /**
+ * Get {@link reverseResolvePattern}
+ *
+ * @return string
+ */
+ protected function getReverseResolvePattern()
+ {
+ if ($this->reverseResolvePattern === null) {
+ $result = ['/\A']; // kind of string builder
+ $macro = false;
+ $macros = [];
+ $currentCapturedSubPatternIndex = 0;
+
+ foreach ($this->template as $part) {
+ if ($macro) {
+ if (isset($macros[$part])) {
+ $result[] = '\g{';
+ $result[] = $macros[$part];
+ $result[] = '}';
+ } else {
+ $macros[$part] = ++$currentCapturedSubPatternIndex;
+ $result[] = '(.*)';
+ }
+ } else {
+ $result[] = preg_quote($part, '/');
+ }
+
+ $macro = ! $macro;
+ }
+
+ $result[] = '\z/s';
+
+ $this->reverseResolvePattern = implode($result);
+ }
+
+ return $this->reverseResolvePattern;
+ }
+}
diff --git a/library/Graphite/Util/TimeRangePickerTools.php b/library/Graphite/Util/TimeRangePickerTools.php
new file mode 100644
index 0000000..d1ebc75
--- /dev/null
+++ b/library/Graphite/Util/TimeRangePickerTools.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Icinga\Module\Graphite\Util;
+
+use Icinga\Application\Config;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+
+final class TimeRangePickerTools
+{
+ /**
+ * @return string
+ */
+ public static function getRelativeRangeParameter()
+ {
+ return 'graph_range';
+ }
+
+ /**
+ * @return string[]
+ */
+ public static function getAbsoluteRangeParameters()
+ {
+ return ['start' => 'graph_start', 'end' => 'graph_end'];
+ }
+
+ /**
+ * @return string[]
+ */
+ public static function getAllRangeParameters()
+ {
+ return array_values(array_merge([static::getRelativeRangeParameter()], static::getAbsoluteRangeParameters()));
+ }
+
+ /**
+ * Copy {@link getAllRangeParameters()} from one {@link UrlParams} instance to another
+ *
+ * @param UrlParams|null $copy Defaults to a new instance
+ * @param UrlParams|null $origin Defaults to the current request's params
+ *
+ * @return UrlParams The copy
+ */
+ public static function copyAllRangeParameters(UrlParams $copy = null, UrlParams $origin = null)
+ {
+ if ($origin === null) {
+ $origin = Url::fromRequest()->getParams();
+ }
+ if ($copy === null) {
+ $copy = new UrlParams();
+ }
+
+ foreach (self::getAllRangeParameters() as $param) {
+ $value = $origin->get($param);
+ if ($value !== null) {
+ $copy->set($param, $value);
+ }
+ }
+
+ return $copy;
+ }
+
+ /**
+ * Extract the relative time range (if any) from the given URL parameters
+ *
+ * @param UrlParams $params
+ *
+ * @return bool|int|null
+ */
+ public static function getRelativeSeconds(UrlParams $params)
+ {
+ $seconds = $params->get(self::getRelativeRangeParameter());
+ if ($seconds === null) {
+ return null;
+ }
+
+ return preg_match('/^(?:0|[1-9]\d*)$/', $seconds) ? (int) $seconds : false;
+ }
+
+ /**
+ * Get the default relative time range for graphs
+ *
+ * @return int
+ *
+ * @throws ConfigurationError
+ */
+ public static function getDefaultRelativeTimeRange()
+ {
+ $rangeFactors = [
+ 'minutes' => 60,
+ 'hours' => 3600,
+ 'days' => 86400,
+ 'weeks' => 604800,
+ 'months' => 2592000,
+ 'years' => 31557600
+ ];
+
+ $config = Config::module('graphite');
+ $unit = $config->get('ui', 'default_time_range_unit', 'hours');
+
+ if (! isset($rangeFactors[$unit])) {
+ throw new ConfigurationError(
+ 'Bad ui.default_time_range_unit %s in file %s',
+ var_export($unit, true),
+ var_export($config->getConfigFile(), true)
+ );
+ }
+
+ return (int) $config->get('ui', 'default_time_range', 1) * $rangeFactors[$unit];
+ }
+}
diff --git a/library/Graphite/Web/Controller/IcingadbGraphiteController.php b/library/Graphite/Web/Controller/IcingadbGraphiteController.php
new file mode 100644
index 0000000..36bc026
--- /dev/null
+++ b/library/Graphite/Web/Controller/IcingadbGraphiteController.php
@@ -0,0 +1,110 @@
+<?php
+
+/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Graphite\Web\Controller;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Common\SearchControls;
+use ipl\Orm\Query;
+use ipl\Stdlib\Contract\Paginatable;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Control\LimitControl;
+use ipl\Web\Control\PaginationControl;
+use ipl\Web\Control\SortControl;
+use ipl\Web\Filter\QueryString;
+use ipl\Stdlib\Filter;
+use ipl\Web\Url;
+
+class IcingadbGraphiteController extends CompatController
+{
+ use Auth;
+ use Database;
+ use SearchControls;
+
+ /** @var bool Whether to use icingadb as the backend */
+ protected $useIcingadbAsBackend;
+
+ /** @var string[] Graph parameters */
+ protected $graphParams = ['graphs_limit', 'graph_range', 'graph_start', 'graph_end', 'legacyParams'];
+
+ /** @var Filter\Rule Filter from query string parameters */
+ private $filter;
+
+ protected function moduleInit()
+ {
+ $this->useIcingadbAsBackend = Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend();
+ }
+
+ /**
+ * Get the filter created from query string parameters
+ *
+ * @return Filter\Rule
+ */
+ public function getFilter(): Filter\Rule
+ {
+ if ($this->filter === null) {
+ $this->filter = QueryString::parse((string) $this->params);
+ }
+
+ return $this->filter;
+ }
+
+ /**
+ * Create and return the LimitControl
+ *
+ * This automatically shifts the limit URL parameter from {@link $params}.
+ *
+ * @return LimitControl
+ */
+ public function createLimitControl(): LimitControl
+ {
+ $limitControl = new LimitControl(Url::fromRequest());
+ $limitControl->setDefaultLimit($this->getPageSize(null));
+
+ $this->params->shift($limitControl->getLimitParam());
+
+ return $limitControl;
+ }
+
+ /**
+ * Create and return the PaginationControl
+ *
+ * This automatically shifts the pagination URL parameters from {@link $params}.
+ *
+ * @return PaginationControl
+ */
+ public function createPaginationControl(Paginatable $paginatable): PaginationControl
+ {
+ $paginationControl = new PaginationControl($paginatable, Url::fromRequest());
+ $paginationControl->setDefaultPageSize($this->getPageSize(null));
+ $paginationControl->setAttribute('id', $this->getRequest()->protectId('pagination-control'));
+
+ $this->params->shift($paginationControl->getPageParam());
+ $this->params->shift($paginationControl->getPageSizeParam());
+
+ return $paginationControl->apply();
+ }
+
+ /**
+ * Create and return the SortControl
+ *
+ * This automatically shifts the sort URL parameter from {@link $params}.
+ *
+ * @param Query $query
+ * @param array $columns Possible sort columns as sort string-label pairs
+ *
+ * @return SortControl
+ */
+ public function createSortControl(Query $query, array $columns): SortControl
+ {
+ $sortControl = SortControl::create($columns);
+
+ $this->params->shift($sortControl->getSortParam());
+
+ return $sortControl->apply($query);
+ }
+}
diff --git a/library/Graphite/Web/Controller/MonitoringAwareController.php b/library/Graphite/Web/Controller/MonitoringAwareController.php
new file mode 100644
index 0000000..dca2ebd
--- /dev/null
+++ b/library/Graphite/Web/Controller/MonitoringAwareController.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Controller;
+
+use ArrayIterator;
+use Icinga\Application\Modules\Module;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filterable;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Data\CustomvarProtectionIterator;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Util\Json;
+use Icinga\File\Csv;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+
+abstract class MonitoringAwareController extends Controller
+{
+ /** @var bool Whether to use icingadb as the backend */
+ protected $useIcingadbAsBackend = false;
+
+ /**
+ * Restrict the given monitored object query for the currently authenticated user
+ *
+ * @param DataView $dataView
+ *
+ * @return DataView The given data view
+ */
+ protected function applyMonitoringRestriction(DataView $dataView)
+ {
+ $this->applyRestriction('monitoring/filter/objects', $dataView);
+
+ return $dataView;
+ }
+
+ protected function moduleInit()
+ {
+ if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) {
+ $this->useIcingadbAsBackend = true;
+
+ return;
+ }
+
+ $this->backend = MonitoringBackend::instance($this->_getParam('backend'));
+ $this->view->url = Url::fromRequest();
+ }
+
+
+ protected function handleFormatRequest($query)
+ {
+ $desiredContentType = $this->getRequest()->getHeader('Accept');
+ if ($desiredContentType === 'application/json') {
+ $desiredFormat = 'json';
+ } elseif ($desiredContentType === 'text/csv') {
+ $desiredFormat = 'csv';
+ } else {
+ $desiredFormat = strtolower($this->params->get('format', 'html'));
+ }
+
+ if ($desiredFormat !== 'html' && ! $this->params->has('limit')) {
+ $query->limit(); // Resets any default limit and offset
+ }
+
+ switch ($desiredFormat) {
+ case 'sql':
+ echo '<pre>'
+ . htmlspecialchars(wordwrap($query->dump()))
+ . '</pre>';
+ exit;
+ case 'json':
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'application/json')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'inline; filename=' . $this->getRequest()->getActionName() . '.json'
+ )
+ ->appendBody(
+ Json::sanitize(
+ iterator_to_array(
+ new CustomvarProtectionIterator(
+ new ArrayIterator($query->fetchAll())
+ )
+ )
+ )
+ )
+ ->sendResponse();
+ exit;
+ case 'csv':
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'text/csv')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv'
+ )
+ ->appendBody((string) Csv::fromQuery(new CustomvarProtectionIterator($query)))
+ ->sendResponse();
+ exit;
+ }
+ }
+
+ /**
+ * Apply a restriction of the authenticated on the given filterable
+ *
+ * @param string $name Name of the restriction
+ * @param Filterable $filterable Filterable to restrict
+ *
+ * @return Filterable The filterable having the restriction applied
+ */
+ protected function applyRestriction($name, Filterable $filterable)
+ {
+ $filterable->applyFilter($this->getRestriction($name));
+ return $filterable;
+ }
+
+ /**
+ * Get a restriction of the authenticated
+ *
+ * @param string $name Name of the restriction
+ *
+ * @return Filter Filter object
+ * @throws ConfigurationError If the restriction contains invalid filter columns
+ */
+ protected function getRestriction($name)
+ {
+ $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);
+ }
+ ));
+ foreach ($this->getRestrictions($name) as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAll();
+ }
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ $this->translate(
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s'
+ ),
+ $name,
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ if ($restriction->isEmpty()) {
+ return Filter::matchAll();
+ }
+
+ return $restriction;
+ }
+}
diff --git a/library/Graphite/Web/Controller/TimeRangePickerTrait.php b/library/Graphite/Web/Controller/TimeRangePickerTrait.php
new file mode 100644
index 0000000..7352b1b
--- /dev/null
+++ b/library/Graphite/Web/Controller/TimeRangePickerTrait.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Controller;
+
+use Icinga\Module\Graphite\Forms\TimeRangePicker\CommonForm;
+use Icinga\Module\Graphite\Forms\TimeRangePicker\CustomForm;
+use Icinga\Web\Request;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+trait TimeRangePickerTrait
+{
+ /**
+ * @var CommonForm
+ */
+ protected $timeRangePickerCommonForm;
+
+ /**
+ * @var CustomForm
+ */
+ protected $timeRangePickerCustomForm;
+
+ /**
+ * Process the given request using the forms
+ *
+ * @param Request $request The request to be processed
+ *
+ * @return Request The request supposed to be processed
+ */
+ protected function handleTimeRangePickerRequest(Request $request = null)
+ {
+ $this->getTimeRangePickerCommonForm()->handleRequest($request);
+ return $this->getTimeRangePickerCustomForm()->handleRequest($request);
+ }
+
+ /**
+ * Render all needed forms and links
+ *
+ * @param View $view
+ *
+ * @return string
+ */
+ protected function renderTimeRangePicker(View $view)
+ {
+ $url = Url::fromRequest()->getAbsoluteUrl();
+
+ return '<div class="timerangepicker-container">'
+ . $this->getTimeRangePickerCommonForm()
+ . '<div class="flyover flyover-arrow-top" data-flyover-suspends-auto-refresh id="'
+ . $view->protectId('graphite-customrange')
+ . '">'
+ . $view->qlink(null, '#', null, [
+ 'title' => $view->translate('Specify custom time range'),
+ 'class' => 'button-link flyover-toggle',
+ 'icon' => 'service'
+ ])
+ . '<div class="flyover-content">' . $this->getTimeRangePickerCustomForm() . '</div>'
+ . '</div>'
+ . '</div>';
+ }
+
+ /**
+ * Get {@link timeRangePickerCommonForm}
+ *
+ * @return CommonForm
+ */
+ public function getTimeRangePickerCommonForm()
+ {
+ if ($this->timeRangePickerCommonForm === null) {
+ $this->timeRangePickerCommonForm = new CommonForm();
+ }
+
+ return $this->timeRangePickerCommonForm;
+ }
+
+ /**
+ * Set {@link timeRangePickerCommonForm}
+ *
+ * @param CommonForm $timeRangePickerCommonForm
+ *
+ * @return $this
+ */
+ public function setTimeRangePickerCommonForm(CommonForm $timeRangePickerCommonForm)
+ {
+ $this->timeRangePickerCommonForm = $timeRangePickerCommonForm;
+ return $this;
+ }
+
+ /**
+ * Get {@link timeRangePickerCustomForm}
+ *
+ * @return CustomForm
+ */
+ public function getTimeRangePickerCustomForm()
+ {
+ if ($this->timeRangePickerCustomForm === null) {
+ $this->timeRangePickerCustomForm = new CustomForm();
+ }
+
+ return $this->timeRangePickerCustomForm;
+ }
+
+ /**
+ * Set {@link timeRangePickerCustomForm}
+ *
+ * @param CustomForm $timeRangePickerCustomForm
+ *
+ * @return $this
+ */
+ public function setTimeRangePickerCustomForm(CustomForm $timeRangePickerCustomForm)
+ {
+ $this->timeRangePickerCustomForm = $timeRangePickerCustomForm;
+ return $this;
+ }
+}
diff --git a/library/Graphite/Web/FakeSchemeRequest.php b/library/Graphite/Web/FakeSchemeRequest.php
new file mode 100644
index 0000000..dc415cd
--- /dev/null
+++ b/library/Graphite/Web/FakeSchemeRequest.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web;
+
+use Icinga\Web\Request;
+
+/**
+ * Rationale:
+ *
+ * {@link Url::fromPath()} doesn't preserve URLs which seem to be internal as they are.
+ */
+class FakeSchemeRequest extends Request
+{
+ public function getScheme()
+ {
+ return 'a random url scheme which always differs from the current request\'s one';
+ }
+}
diff --git a/library/Graphite/Web/Form/Decorator/Proxy.php b/library/Graphite/Web/Form/Decorator/Proxy.php
new file mode 100644
index 0000000..63d339c
--- /dev/null
+++ b/library/Graphite/Web/Form/Decorator/Proxy.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Zend_Form_Decorator_Interface;
+
+/**
+ * Wrap a decorator and use it only for rendering
+ */
+class Proxy extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * The actual decorator being proxied
+ *
+ * @var Zend_Form_Decorator_Interface
+ */
+ protected $actualDecorator;
+
+ public function render($content)
+ {
+ return $this->actualDecorator->render($content);
+ }
+
+ /**
+ * Get {@link actualDecorator}
+ *
+ * @return Zend_Form_Decorator_Interface
+ */
+ public function getActualDecorator()
+ {
+ return $this->actualDecorator;
+ }
+
+ /**
+ * Set {@link actualDecorator}
+ *
+ * @param Zend_Form_Decorator_Interface $actualDecorator
+ *
+ * @return $this
+ */
+ public function setActualDecorator($actualDecorator)
+ {
+ $this->actualDecorator = $actualDecorator;
+ return $this;
+ }
+}
diff --git a/library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php b/library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php
new file mode 100644
index 0000000..893a5b7
--- /dev/null
+++ b/library/Graphite/Web/Form/Validator/CustomErrorMessagesValidator.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Provides an easy way to implement validators with custom error messages
+ *
+ * TODO(ak): move to framework(?)
+ */
+abstract class CustomErrorMessagesValidator extends Zend_Validate_Abstract
+{
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->_messageTemplates = ['CUSTOM_ERROR' => ''];
+ }
+
+ public function isValid($value)
+ {
+ $errorMessage = $this->validate($value);
+ if ($errorMessage === null) {
+ return true;
+ }
+
+ $this->setMessage($errorMessage, 'CUSTOM_ERROR');
+ $this->_error('CUSTOM_ERROR');
+ return false;
+ }
+
+ /**
+ * Validate the given value and return an error message if it's invalid
+ *
+ * @param string $value
+ *
+ * @return string|null
+ */
+ abstract protected function validate($value);
+}
diff --git a/library/Graphite/Web/Form/Validator/HttpUserValidator.php b/library/Graphite/Web/Form/Validator/HttpUserValidator.php
new file mode 100644
index 0000000..d5f4a86
--- /dev/null
+++ b/library/Graphite/Web/Form/Validator/HttpUserValidator.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validates http basic authn user names
+ *
+ * TODO(ak): move to Icinga Web 2
+ */
+class HttpUserValidator extends Zend_Validate_Abstract
+{
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->_messageTemplates = ['HAS_COLON' => mt('graphite', 'The username must not contain colons.')];
+ }
+
+ public function isValid($value)
+ {
+ $hasColon = false !== strpos($value, ':');
+ if ($hasColon) {
+ $this->_error('HAS_COLON');
+ }
+ return ! $hasColon;
+ }
+}
diff --git a/library/Graphite/Web/Form/Validator/MacroTemplateValidator.php b/library/Graphite/Web/Form/Validator/MacroTemplateValidator.php
new file mode 100644
index 0000000..8ff4e3c
--- /dev/null
+++ b/library/Graphite/Web/Form/Validator/MacroTemplateValidator.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Form\Validator;
+
+use Icinga\Module\Graphite\Util\MacroTemplate;
+use InvalidArgumentException;
+
+/**
+ * Validates Icinga-style macro templates
+ */
+class MacroTemplateValidator extends CustomErrorMessagesValidator
+{
+ protected function validate($value)
+ {
+ try {
+ new MacroTemplate($value);
+ } catch (InvalidArgumentException $e) {
+ return $e->getMessage();
+ }
+ }
+}
diff --git a/library/Graphite/Web/Widget/GraphImage.php b/library/Graphite/Web/Widget/GraphImage.php
new file mode 100644
index 0000000..af64e69
--- /dev/null
+++ b/library/Graphite/Web/Widget/GraphImage.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget;
+
+use Icinga\Module\Graphite\Graphing\Chart;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+use Icinga\Web\Widget\AbstractWidget;
+use RuntimeException;
+
+class GraphImage extends AbstractWidget
+{
+ /**
+ * The chart to be rendered
+ *
+ * @var Chart
+ */
+ protected $chart;
+
+ /**
+ * The rendered PNG image
+ *
+ * @var string|null
+ */
+ protected $rendered;
+
+ /**
+ * Constructor
+ *
+ * @param Chart $chart The chart to be rendered
+ */
+ public function __construct(Chart $chart)
+ {
+ $this->chart = $chart;
+ }
+
+ /**
+ * Render the graph lazily
+ *
+ * @return string
+ */
+ public function render()
+ {
+ if ($this->rendered === null) {
+ $now = time();
+
+ $from = (int) $this->chart->getFrom();
+ if ($from < 0) {
+ $from += $now;
+ }
+
+ $until = (string) $this->chart->getUntil();
+
+ if ($until === '') {
+ $until = $now;
+ } else {
+ $until = (int) $until;
+ if ($until < 0) {
+ $until += $now;
+ }
+ }
+
+ $variables = $this->chart->getMetricVariables();
+ $template = $this->chart->getTemplate();
+ $graphiteWebClient = $this->chart->getGraphiteWebClient();
+ $params = (new UrlParams())->addValues([
+ 'from' => $from,
+ 'until' => $until,
+ 'bgcolor' => $this->chart->getBackgroundColor() ?? 'black',
+ 'fgcolor' => $this->chart->getForegroundColor() ?? 'white',
+ 'majorGridLineColor' => $this->chart->getMajorGridLineColor() ?? '0000003F',
+ 'minorGridLineColor' => $this->chart->getMinorGridLineColor() ?? 'black',
+ 'width' => $this->chart->getWidth(),
+ 'height' => $this->chart->getHeight(),
+ 'hideLegend' => (string) ! $this->chart->getShowLegend(),
+ 'tz' => date_default_timezone_get(),
+ '_salt' => "$now.000",
+ 'vTitle' => 'Percent',
+ 'lineMode' => 'connected',
+ 'drawNullAsZero' => 'false',
+ 'graphType' => 'line',
+ '_ext' => 'whatever.svg'
+ ]);
+
+ foreach ($template->getUrlParams() as $key => $value) {
+ $params->set($key, $value->resolve($variables));
+ }
+
+ $metrics = $this->chart->getMetrics();
+ $allVars = [];
+
+ foreach ($template->getCurves() as $curveName => $curve) {
+ if (!isset($metrics[$curveName])) {
+ continue;
+ }
+
+ $vars = $curve[0]->reverseResolve($metrics[$curveName]);
+
+ if ($vars !== false) {
+ $allVars = array_merge($allVars, $vars);
+ }
+ }
+
+ foreach ($metrics as $curveName => $metric) {
+ $allVars['metric'] = $metric;
+ $params->add('target', $template->getCurves()[$curveName][1]->resolve($allVars));
+ }
+
+ $url = Url::fromPath('/render')->setParams($params);
+ $headers = [
+ 'Accept-language' => 'en',
+ 'Content-type' => 'application/x-www-form-urlencoded'
+ ];
+
+ for (;;) {
+ try {
+ $this->rendered = $graphiteWebClient->request($url, 'GET', $headers);
+ } catch (RuntimeException $e) {
+ if (preg_match('/\b500\b/', $e->getMessage())) {
+ // A 500 Internal Server Error, probably because of
+ // a division by zero because of a too low time range to render.
+
+ $until = (int) $url->getParam('until');
+ $diff = $until - (int) $url->getParam('from');
+
+ // Try to render a higher time range, but give up
+ // once our default (1h) has been reached (non successfully).
+ if ($diff < 3600) {
+ $url->setParam('from', $until - $diff * 2);
+ continue;
+ }
+ }
+
+ throw $e;
+ }
+
+ break;
+ }
+ }
+
+ return $this->rendered;
+ }
+}
diff --git a/library/Graphite/Web/Widget/Graphs.php b/library/Graphite/Web/Widget/Graphs.php
new file mode 100644
index 0000000..e18b8da
--- /dev/null
+++ b/library/Graphite/Web/Widget/Graphs.php
@@ -0,0 +1,688 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Graphite\Graphing\Chart;
+use Icinga\Module\Graphite\Graphing\GraphingTrait;
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Util\InternalProcessTracker as IPT;
+use Icinga\Module\Graphite\Util\TimeRangePickerTools;
+use Icinga\Module\Graphite\Web\Widget\Graphs\Host as HostGraphs;
+use Icinga\Module\Graphite\Web\Widget\Graphs\Service as ServiceGraphs;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Request;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+use Icinga\Web\Widget\AbstractWidget;
+use ipl\Orm\Model;
+use Icinga\Module\Icingadb\Model\Host as IcingadbHost;
+use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbHost as IcingadbHostGraphs;
+use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbService as IcingadbServiceGraphs;
+
+abstract class Graphs extends AbstractWidget
+{
+ use GraphingTrait;
+
+ /**
+ * The Icinga custom variable with the "real" check command (if any) of objects we display graphs for
+ *
+ * @var string
+ */
+ protected static $obscuredCheckCommandCustomVar;
+
+ /**
+ * The type of the object to render the graphs for
+ *
+ * @var string
+ */
+ protected $objectType;
+
+ /**
+ * The object to render the graphs for
+ *
+ * @var MonitoredObject|Model
+ */
+ protected $object;
+
+ /**
+ * Graph image width
+ *
+ * @var string
+ */
+ protected $width = '350';
+
+ /**
+ * Graph image height
+ *
+ * @var string
+ */
+ protected $height = '200';
+
+ /**
+ * Graph range start
+ *
+ * @var string
+ */
+ protected $start;
+
+ /**
+ * Graph range end
+ *
+ * @var string
+ */
+ protected $end;
+
+ /**
+ * Whether to render as compact as possible
+ *
+ * @var bool
+ */
+ protected $compact = false;
+
+ /**
+ * The check command of the monitored object we display graphs for
+ *
+ * @var string
+ */
+ protected $checkCommand;
+
+ /**
+ * The "real" check command (if any) of the monitored object we display graphs for
+ *
+ * E.g. the command executed remotely via check_by_ssh
+ *
+ * @var string|null
+ */
+ protected $obscuredCheckCommand;
+
+ /**
+ * Additional CSS classes for the <div/>s around the images
+ *
+ * @var string[]
+ */
+ protected $classes = [];
+
+ /**
+ * Whether to serve a transparent dummy image first and let the JS code load the actual graph
+ *
+ * @var bool
+ */
+ protected $preloadDummy = false;
+
+ /**
+ * Whether to render the graphs inline
+ *
+ * @var bool
+ */
+ protected $renderInline;
+
+ /**
+ * Whether to explicitly display that no graphs were found
+ *
+ * @var bool|null
+ */
+ protected $showNoGraphsFound;
+
+ /**
+ * Factory, based on the given monitoring object
+ *
+ * @param MonitoredObject $object
+ *
+ * @return static
+ */
+ public static function forMonitoredObject(MonitoredObject $object)
+ {
+ switch ($object->getType()) {
+ case 'host':
+ /** @var Host $object */
+ return new HostGraphs($object);
+
+ case 'service':
+ /** @var Service $object */
+ return new ServiceGraphs($object);
+ }
+ }
+
+ /**
+ * Factory, based on the given icingadb object
+ *
+ * @param Model $object
+ *
+ * @return static
+ */
+ public static function forIcingadbObject(Model $object)
+ {
+ if ($object instanceof IcingadbHost) {
+ return new IcingadbHostGraphs($object);
+ }
+
+ return new IcingadbServiceGraphs($object);
+ }
+
+ /**
+ * Get the Icinga custom variable with the "real" check command (if any) of monitored objects we display graphs for
+ *
+ * @return string
+ */
+ public static function getObscuredCheckCommandCustomVar()
+ {
+ if (static::$obscuredCheckCommandCustomVar === null) {
+ static::$obscuredCheckCommandCustomVar = Config::module('graphite')
+ ->get('icinga', 'customvar_obscured_check_command', 'check_command');
+ }
+
+ return static::$obscuredCheckCommandCustomVar;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param MonitoredObject|Model $object The object to render the graphs for
+ */
+ public function __construct($object)
+ {
+ $this->object = $object;
+ $this->renderInline = Url::fromRequest()->getParam('format') === 'pdf';
+
+ if ($object instanceof Model) {
+ $this->checkCommand = $object->checkcommand_name;
+ $this->obscuredCheckCommand = $object->vars[Graphs::getObscuredCheckCommandCustomVar()] ?? null;
+ } else {
+ $this->checkCommand = $object->{"{$this->objectType}_check_command"};
+ $this->obscuredCheckCommand = $object->{
+ "_{$this->objectType}_" . Graphs::getObscuredCheckCommandCustomVar()
+ };
+ }
+ }
+
+ /**
+ * Process the given request using this widget
+ *
+ * @param Request $request The request to be processed
+ *
+ * @return $this
+ */
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = Icinga::app()->getRequest();
+ }
+
+ $params = $request->getUrl()->getParams();
+ list($this->start, $this->end) = $this->getRangeFromTimeRangePicker($request);
+ $this->width = $params->shift('width', $this->width);
+ $this->height = $params->shift('height', $this->height);
+
+ return $this;
+ }
+
+ /**
+ * Render the graphs list
+ *
+ * @return string
+ */
+ protected function getGraphsList()
+ {
+ $result = []; // kind of string builder
+ $imageBaseUrl = $this->getImageBaseUrl();
+ $allTemplates = $this->getAllTemplates();
+ $actualCheckCommand = $this->obscuredCheckCommand === null ? $this->checkCommand : $this->obscuredCheckCommand;
+ $concreteTemplates = $allTemplates->getTemplates($actualCheckCommand);
+
+ $excludedMetrics = [];
+
+ foreach ($concreteTemplates as $concreteTemplate) {
+ foreach ($concreteTemplate->getCurves() as $curve) {
+ $excludedMetrics[] = $curve[0];
+ }
+ }
+
+ IPT::recordf("Icinga check command: %s", $this->checkCommand);
+ IPT::recordf("Obscured check command: %s", $this->obscuredCheckCommand);
+
+ foreach (
+ [
+ ['template', $concreteTemplates, []],
+ ['default_template', $allTemplates->getDefaultTemplates(), $excludedMetrics],
+ ] as $templateSet
+ ) {
+ list($urlParam, $templates, $excludeMetrics) = $templateSet;
+
+ if ($urlParam === 'template') {
+ IPT::recordf('Applying templates for check command %s', $actualCheckCommand);
+ } else {
+ IPT::recordf('Applying default templates, excluding previously used metrics');
+ }
+
+ IPT::indent();
+
+ foreach ($templates as $templateName => $template) {
+ if ($this->designedForObjectType($template)) {
+ IPT::recordf('Applying template %s', $templateName);
+ IPT::indent();
+
+ $charts = $template->getCharts(
+ static::getMetricsDataSource(),
+ $this->object,
+ [],
+ $excludeMetrics
+ );
+
+ if (! empty($charts)) {
+ $currentGraphs = [];
+
+ foreach ($charts as $chart) {
+ /** @var Chart $chart */
+
+ $metricVariables = $chart->getMetricVariables();
+ $bestIntersect = -1;
+ $bestPos = count($result);
+
+ foreach ($result as $graphPos => & $graph) {
+ $currentIntersect = count(array_intersect_assoc($graph[1], $metricVariables));
+
+ if ($currentIntersect >= $bestIntersect) {
+ $bestIntersect = $currentIntersect;
+ $bestPos = $graphPos + 1;
+ }
+ }
+ unset($graph);
+
+ $urlParams = $template->getUrlParams();
+ if (array_key_exists("height", $urlParams)) {
+ $actheight = $urlParams["height"]->resolve(['height']);
+ if ($actheight < $this->height) {
+ $actheight = $this->height;
+ }
+ } else {
+ $actheight = $this->height;
+ }
+ $actwidth = $this->width;
+ $actwidthfix = "";
+ if (array_key_exists("width", $urlParams)) {
+ $actwidth = $urlParams["width"]->resolve(['width']);
+ $actwidthfix = "width: {$actwidth}px; ";
+ }
+
+ if ($this->renderInline) {
+ $chart->setFrom($this->start)
+ ->setUntil($this->end)
+ ->setWidth($actwidth)
+ ->setHeight($actheight)
+ ->setBackgroundColor('white')
+ ->setForegroundColor('black')
+ ->setMajorGridLineColor('grey')
+ ->setMinorGridLineColor('white')
+ ->setShowLegend(! $this->compact);
+
+ $img = new InlineGraphImage($chart);
+ } else {
+ $imageUrl = $this->filterImageUrl($imageBaseUrl->with($metricVariables))
+ ->setParam($urlParam, $templateName)
+ ->setParam('start', $this->start)
+ ->setParam('end', $this->end)
+ ->setParam('width', $actwidth)
+ ->setParam('height', $actheight);
+
+ if (! $this->compact) {
+ $imageUrl->setParam('legend', 1);
+ }
+
+ if ($this->preloadDummy) {
+ $src = 'data:image/png;base64,' // 1x1 dummy
+ . 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRS'
+ . 'TlMAQObYZgAAAApJREFUeAFjZAAAAAQAAhq+CAMAAAAASUVORK5CYII=';
+ } else {
+ $src = $imageUrl;
+ }
+
+ $img = '<img id="graphiteImg-' . md5((string) $imageUrl) . '"'
+ . " src=\"$src\" data-actualimageurl=\"$imageUrl\" class=\"detach graphiteImg\""
+ . " alt=\"\" width=\"$actwidth\" height=\"$actheight\""
+ . " style=\"min-width: {$actwidth}px; $actwidthfix min-height: {$actheight}px;\">";
+ }
+
+ $currentGraphs[] = [$img, $metricVariables, $bestPos];
+ }
+
+ foreach (array_reverse($currentGraphs) as $graph) {
+ list($img, $metricVariables, $bestPos) = $graph;
+ array_splice($result, $bestPos, 0, [[$img, $metricVariables]]);
+ }
+ }
+
+ IPT::unindent();
+ } else {
+ IPT::recordf('Not applying template %s', $templateName);
+ }
+ }
+
+ IPT::unindent();
+ }
+
+ if (! empty($result)) {
+ foreach ($result as & $graph) {
+ $graph = $graph[0];
+ }
+ unset($graph);
+
+ $currentUrl = Icinga::app()->getRequest()->getUrl();
+ $limit = (int) $currentUrl->getParam('graphs_limit', 2);
+ $total = count($result);
+
+ if ($limit < 1) {
+ $limit = -1;
+ }
+
+ if ($limit !== -1 && $total > $limit) {
+ $result = array_slice($result, 0, $limit);
+
+ if (! $this->compact) {
+ /** @var View $view */
+ $view = $this->view();
+
+ $url = $this->getGraphsListBaseUrl();
+ TimeRangePickerTools::copyAllRangeParameters($url->getParams(), $currentUrl->getParams());
+
+ $result[] = "<p class='load-more'>{$view->qlink(
+ sprintf($view->translate('Load all %d graphs'), $total),
+ $url->setParam('graphs_limit', '-1'),
+ null,
+ ['class' => 'action-link']
+ )}</p>";
+ }
+ }
+
+ $classes = $this->classes;
+ $classes[] = 'images';
+
+ array_unshift($result, '<div class="' . implode(' ', $classes) . '">');
+ $result[] = '</div>';
+ }
+
+ if ($this->renderInline) {
+ foreach ($result as $html) {
+ if ($html instanceof InlineGraphImage) {
+ // Errors should occur now or not at all
+ $html->render();
+ }
+ }
+ }
+
+ return implode($result);
+ }
+
+ public function render()
+ {
+ IPT::clear();
+
+ try {
+ $result = $this->getGraphsList();
+ } catch (ConfigurationError $e) {
+ $view = $this->view();
+
+ return "<p>{$view->escape($e->getMessage())}</p>"
+ . '<p>' . vsprintf(
+ $view->escape($view->translate('Please %scorrect%s the configuration of the Graphite module.')),
+ Auth::getInstance()->hasPermission('config/modules')
+ ? explode(
+ '$LINK_TEXT$',
+ $view->qlink('$LINK_TEXT$', 'graphite/config/backend', null, ['class' => 'action-link'])
+ )
+ : ['', '']
+ ) . '</p>';
+ }
+
+ $view = $this->view();
+
+ if ($result === '' && $this->getShowNoGraphsFound()) {
+ $result = "<p>{$view->escape($view->translate('No graphs found'))}</p>";
+ }
+
+ if (IPT::enabled()) {
+ $result .= "<h3>{$view->escape($view->translate('Graphs assembling process record'))}</h3>"
+ . "<pre>{$view->escape(IPT::dump())}</pre>";
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get time range parameters for Graphite from the URL
+ *
+ * @param Request $request The request to be used
+ *
+ * @return string[]
+ */
+ protected function getRangeFromTimeRangePicker(Request $request)
+ {
+ $params = $request->getUrl()->getParams();
+ $relative = $params->get(TimeRangePickerTools::getRelativeRangeParameter());
+ if ($relative !== null) {
+ return ["-$relative", null];
+ }
+
+ $absolute = TimeRangePickerTools::getAbsoluteRangeParameters();
+ $start = $params->get($absolute['start']);
+ return [
+ $start === null ? -TimeRangePickerTools::getDefaultRelativeTimeRange() : $start,
+ $params->get($absolute['end'])
+ ];
+ }
+
+ /**
+ * Return a identifier specifying the monitored object we display graphs for
+ *
+ * @return string
+ */
+ abstract protected function getMonitoredObjectIdentifier();
+
+ /**
+ * Get the base URL to a graph specifying just the monitored object kind
+ *
+ * @return Url
+ */
+ abstract protected function getImageBaseUrl();
+
+ /**
+ * Get the base URL to the monitored object's graphs list
+ *
+ * @return Url
+ */
+ abstract protected function getGraphsListBaseUrl();
+
+ /**
+ * Extend the {@link getImageBaseUrl()}'s result's parameters with the concrete monitored object
+ *
+ * @param Url $url The URL to extend
+ *
+ * @return Url The given URL
+ */
+ abstract protected function filterImageUrl(Url $url);
+
+ /**
+ * Return whether the given template is designed for the type of the object we display graphs for
+ *
+ * @param Template $template
+ *
+ * @return bool
+ */
+ abstract protected function designedForObjectType(Template $template);
+
+ /**
+ * Get {@link compact}
+ *
+ * @return bool
+ */
+ public function getCompact()
+ {
+ return $this->compact;
+ }
+
+ /**
+ * Set {@link compact}
+ *
+ * @param bool $compact
+ *
+ * @return $this
+ */
+ public function setCompact($compact = true)
+ {
+ $this->compact = $compact;
+ return $this;
+ }
+
+ /**
+ * Get the graph image width
+ *
+ * @return string
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * Set the graph image width
+ *
+ * @param string $width
+ *
+ * @return $this
+ */
+ public function setWidth($width)
+ {
+ $this->width = $width;
+
+ return $this;
+ }
+
+ /**
+ * Get the graph image height
+ *
+ * @return string
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * Set the graph image height
+ *
+ * @param string $height
+ *
+ * @return $this
+ */
+ public function setHeight($height)
+ {
+ $this->height = $height;
+
+ return $this;
+ }
+
+ /**
+ * Get additional CSS classes for the <div/>s around the images
+ *
+ * @return string[]
+ */
+ public function getClasses()
+ {
+ return $this->classes;
+ }
+
+ /**
+ * Set additional CSS classes for the <div/>s around the images
+ *
+ * @param string[] $classes
+ *
+ * @return $this
+ */
+ public function setClasses($classes)
+ {
+ $this->classes = $classes;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to serve a transparent dummy image first and let the JS code load the actual graph
+ *
+ * @return bool
+ */
+ public function getPreloadDummy()
+ {
+ return $this->preloadDummy;
+ }
+
+ /**
+ * Set whether to serve a transparent dummy image first and let the JS code load the actual graph
+ *
+ * @param bool $preloadDummy
+ *
+ * @return $this
+ */
+ public function setPreloadDummy($preloadDummy = true)
+ {
+ $this->preloadDummy = $preloadDummy;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to render the graphs inline
+ *
+ * @return bool
+ */
+ public function getRenderInline()
+ {
+ return $this->renderInline;
+ }
+
+ /**
+ * Set whether to render the graphs inline
+ *
+ * @param bool $renderInline
+ *
+ * @return $this
+ */
+ public function setRenderInline($renderInline = true)
+ {
+ $this->renderInline = $renderInline;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to explicitly display that no graphs were found
+ *
+ * @return bool
+ */
+ public function getShowNoGraphsFound()
+ {
+ if ($this->showNoGraphsFound === null) {
+ $this->showNoGraphsFound = ! Config::module('graphite')->get('ui', 'disable_no_graphs_found');
+ }
+
+ return $this->showNoGraphsFound;
+ }
+
+ /**
+ * Set whether to explicitly display that no graphs were found
+ *
+ * @param bool $showNoGraphsFound
+ *
+ * @return $this
+ */
+ public function setShowNoGraphsFound($showNoGraphsFound = true)
+ {
+ $this->showNoGraphsFound = $showNoGraphsFound;
+
+ return $this;
+ }
+}
diff --git a/library/Graphite/Web/Widget/Graphs/Host.php b/library/Graphite/Web/Widget/Graphs/Host.php
new file mode 100644
index 0000000..2247bcc
--- /dev/null
+++ b/library/Graphite/Web/Widget/Graphs/Host.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget\Graphs;
+
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Monitoring\Object\Host as MonitoredHost;
+use Icinga\Web\Url;
+
+class Host extends Graphs
+{
+ protected $objectType = 'host';
+
+ /**
+ * The host to render the graphs of
+ *
+ * @var MonitoredHost
+ */
+ protected $object;
+
+ protected function getImageBaseUrl()
+ {
+ return Url::fromPath('graphite/graph/host');
+ }
+
+ protected function getGraphsListBaseUrl()
+ {
+ return Url::fromPath('graphite/list/hosts', ['host' => $this->object->getName()]);
+ }
+
+ protected function filterImageUrl(Url $url)
+ {
+ return $url->setParam('host.name', $this->object->getName());
+ }
+
+ protected function getMonitoredObjectIdentifier()
+ {
+ return $this->object->getName();
+ }
+
+ protected function designedForObjectType(Template $template)
+ {
+ foreach ($template->getCurves() as $curve) {
+ if (in_array('host_name_template', $curve[0]->getMacros())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php
new file mode 100644
index 0000000..2b0a614
--- /dev/null
+++ b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbHost.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb;
+
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Web\Url;
+use Icinga\Module\Icingadb\Model\Host;
+
+class IcingadbHost extends Graphs
+{
+ protected $objectType = 'host';
+
+ /**
+ * The Icingadb host to render the graphs for
+ *
+ * @var Host
+ */
+ protected $object;
+
+ protected function getGraphsListBaseUrl()
+ {
+ return Url::fromPath('graphite/hosts', ['host.name' => $this->object->name]);
+ }
+
+ protected function filterImageUrl(Url $url)
+ {
+ return $url->setParam('host.name', $this->object->name);
+ }
+
+ public function createHostTitle()
+ {
+ return $this->object->name;
+ }
+
+ public function getObjectType()
+ {
+ return $this->objectType;
+ }
+
+ protected function getMonitoredObjectIdentifier()
+ {
+ return $this->object->name;
+ }
+
+ protected function getImageBaseUrl()
+ {
+ return Url::fromPath('graphite/graph/host');
+ }
+
+ protected function designedForObjectType(Template $template)
+ {
+ foreach ($template->getCurves() as $curve) {
+ if (in_array('host_name_template', $curve[0]->getMacros())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php
new file mode 100644
index 0000000..7827e86
--- /dev/null
+++ b/library/Graphite/Web/Widget/Graphs/Icingadb/IcingadbService.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb;
+
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Web\Url;
+use Icinga\Module\Icingadb\Model\Service;
+
+class IcingadbService extends Graphs
+{
+ protected $objectType = 'service';
+
+ /**
+ * The icingadb service to render the graphs for
+ *
+ * @var Service
+ */
+ protected $object;
+
+ protected function getGraphsListBaseUrl()
+ {
+ return Url::fromPath(
+ 'graphite/services',
+ ['service.name' => $this->object->name, 'host.name' => $this->object->host->name]
+ );
+ }
+
+ protected function filterImageUrl(Url $url)
+ {
+ return $url
+ ->setParam('host.name', $this->object->host->name)
+ ->setParam('service.name', $this->object->name);
+ }
+
+ public function createHostTitle()
+ {
+ return $this->object->host->name;
+ }
+
+ public function createServiceTitle()
+ {
+ return ' : ' . $this->object->name;
+ }
+
+ public function getObjectType()
+ {
+ return $this->objectType;
+ }
+
+ protected function getMonitoredObjectIdentifier()
+ {
+ return $this->object->host->name . ':' . $this->object->name;
+ }
+
+ protected function getImageBaseUrl()
+ {
+ return Url::fromPath('graphite/graph/service');
+ }
+
+ protected function designedforObjectType(Template $template)
+ {
+ foreach ($template->getCurves() as $curve) {
+ if (in_array('service_name_template', $curve[0]->getMacros())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/library/Graphite/Web/Widget/Graphs/Service.php b/library/Graphite/Web/Widget/Graphs/Service.php
new file mode 100644
index 0000000..5fc0143
--- /dev/null
+++ b/library/Graphite/Web/Widget/Graphs/Service.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget\Graphs;
+
+use Icinga\Module\Graphite\Graphing\Template;
+use Icinga\Module\Graphite\Web\Widget\Graphs;
+use Icinga\Module\Monitoring\Object\Service as MonitoredService;
+use Icinga\Web\Url;
+
+class Service extends Graphs
+{
+ protected $objectType = 'service';
+
+ /**
+ * The service to render the graphs for
+ *
+ * @var MonitoredService
+ */
+ protected $object;
+
+ protected function getImageBaseUrl()
+ {
+ return Url::fromPath('graphite/graph/service');
+ }
+
+ protected function getGraphsListBaseUrl()
+ {
+ return Url::fromPath(
+ 'graphite/list/services',
+ ['host' => $this->object->getHost()->getName(), 'service' => $this->object->getName()]
+ );
+ }
+
+ protected function filterImageUrl(Url $url)
+ {
+ return $url
+ ->setParam('host.name', $this->object->getHost()->getName())
+ ->setParam('service.name', $this->object->getName());
+ }
+
+ protected function getMonitoredObjectIdentifier()
+ {
+ return $this->object->getHost()->getName() . ':' . $this->object->getName();
+ }
+
+ protected function designedForObjectType(Template $template)
+ {
+ foreach ($template->getCurves() as $curve) {
+ if (in_array('service_name_template', $curve[0]->getMacros())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/library/Graphite/Web/Widget/IcingadbGraphs.php b/library/Graphite/Web/Widget/IcingadbGraphs.php
new file mode 100644
index 0000000..e038e92
--- /dev/null
+++ b/library/Graphite/Web/Widget/IcingadbGraphs.php
@@ -0,0 +1,106 @@
+<?php
+
+/* Icinga Graphite Web | (c) 2022 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Graphite\Web\Widget;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbHost;
+use Icinga\Module\Graphite\Web\Widget\Graphs\Icingadb\IcingadbService;
+use Icinga\Module\Icingadb\Common\Links;
+use Icinga\Module\Icingadb\Widget\EmptyState;
+use Icinga\Module\Icingadb\Model\Host;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlString;
+use ipl\Orm\ResultSet;
+use ipl\Stdlib\BaseFilter;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Widget\Link;
+
+/**
+* Class for creating graphs of icingadb objects
+*/
+class IcingadbGraphs extends BaseHtmlElement
+{
+ use BaseFilter;
+
+ protected $defaultAttributes = ['class' => 'grid'];
+
+ /** @var Iterable */
+ protected $objects;
+
+ protected $tag = 'div';
+
+ /**
+ * Create a new Graph item
+ *
+ * @param ResultSet $objects
+ */
+ public function __construct(ResultSet $objects)
+ {
+ $this->objects = $objects;
+ }
+
+ protected function assemble()
+ {
+ if (! $this->objects->hasResult()) {
+ $this->add(new EmptyState(t('No items found.')));
+ }
+
+ foreach ($this->objects as $object) {
+ $this->add($this->createGridItem($object));
+ }
+
+ $document = new HtmlDocument();
+ $document->addHtml(Html::tag('div', ['class' => 'graphite-graph-color-registry']), $this);
+ $this->prependWrapper($document);
+ }
+
+ protected function createGridItem($object)
+ {
+ if ($object instanceof Host) {
+ $graph = new IcingadbHost($object);
+ $hostObj = $object;
+ } else {
+ $graph = new IcingadbService($object);
+ $hostObj = $object->host;
+ }
+
+ $hostUrl = Links::host($hostObj);
+
+ if ($this->hasBaseFilter()) {
+ $hostUrl->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter())));
+ }
+
+ $hostLink = new Link(
+ $graph->createHostTitle(),
+ $hostUrl,
+ ['data-base-target' => '_next']
+ );
+
+ $serviceLink = null;
+ if ($graph->getObjectType() === 'service') {
+ $serviceUrl = Links::service($object, $hostObj);
+
+ if ($this->hasBaseFilter()) {
+ $serviceUrl->addFilter(Filter::fromQueryString(QueryString::render($this->getBaseFilter())));
+ }
+
+ $serviceLink = new Link(
+ $graph->createServiceTitle(),
+ $serviceUrl,
+ ['data-base-target' => '_next']
+ );
+ }
+
+ $gridItem = Html::tag('div', ['class' => 'grid-item']);
+ $header = Html::tag('h2');
+
+ $header->add([$hostLink, $serviceLink]);
+ $gridItem->add($header);
+
+ return $gridItem->add(HtmlString::create($graph->setPreloadDummy()->handleRequest()));
+ }
+}
diff --git a/library/Graphite/Web/Widget/InlineGraphImage.php b/library/Graphite/Web/Widget/InlineGraphImage.php
new file mode 100644
index 0000000..881384d
--- /dev/null
+++ b/library/Graphite/Web/Widget/InlineGraphImage.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Icinga\Module\Graphite\Web\Widget;
+
+use Icinga\Module\Graphite\Graphing\Chart;
+use Icinga\Web\Widget\AbstractWidget;
+
+class InlineGraphImage extends AbstractWidget
+{
+ /**
+ * The image to be rendered
+ *
+ * @var GraphImage
+ */
+ protected $image;
+
+ /**
+ * The rendered <img>
+ *
+ * @var string|null
+ */
+ protected $rendered;
+
+ /**
+ * Constructor
+ *
+ * @param Chart $chart The chart to be rendered
+ */
+ public function __construct(Chart $chart)
+ {
+ $this->image = new GraphImage($chart);
+ }
+
+ /**
+ * Render the graph lazily
+ *
+ * @return string
+ */
+ public function render()
+ {
+ if ($this->rendered === null) {
+ $this->rendered = '<img src="data:image/png;base64,'
+ . implode("\n", str_split(base64_encode($this->image->render()), 76))
+ . '">';
+ }
+
+ return $this->rendered;
+ }
+}
diff --git a/library/vendor/Psr/Http/Message/MessageInterface.php b/library/vendor/Psr/Http/Message/MessageInterface.php
new file mode 100644
index 0000000..8f67a05
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/MessageInterface.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * HTTP messages consist of requests from a client to a server and responses
+ * from a server to a client. This interface defines the methods common to
+ * each.
+ *
+ * Messages are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ *
+ * @link http://www.ietf.org/rfc/rfc7230.txt
+ * @link http://www.ietf.org/rfc/rfc7231.txt
+ */
+interface MessageInterface
+{
+ /**
+ * Retrieves the HTTP protocol version as a string.
+ *
+ * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+ *
+ * @return string HTTP protocol version.
+ */
+ public function getProtocolVersion();
+
+ /**
+ * Return an instance with the specified HTTP protocol version.
+ *
+ * The version string MUST contain only the HTTP version number (e.g.,
+ * "1.1", "1.0").
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new protocol version.
+ *
+ * @param string $version HTTP protocol version
+ * @return self
+ */
+ public function withProtocolVersion($version);
+
+ /**
+ * Retrieves all message header values.
+ *
+ * The keys represent the header name as it will be sent over the wire, and
+ * each value is an array of strings associated with the header.
+ *
+ * // Represent the headers as a string
+ * foreach ($message->getHeaders() as $name => $values) {
+ * echo $name . ": " . implode(", ", $values);
+ * }
+ *
+ * // Emit headers iteratively:
+ * foreach ($message->getHeaders() as $name => $values) {
+ * foreach ($values as $value) {
+ * header(sprintf('%s: %s', $name, $value), false);
+ * }
+ * }
+ *
+ * While header names are not case-sensitive, getHeaders() will preserve the
+ * exact case in which headers were originally specified.
+ *
+ * @return array Returns an associative array of the message's headers. Each
+ * key MUST be a header name, and each value MUST be an array of strings
+ * for that header.
+ */
+ public function getHeaders();
+
+ /**
+ * Checks if a header exists by the given case-insensitive name.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return bool Returns true if any header names match the given header
+ * name using a case-insensitive string comparison. Returns false if
+ * no matching header name is found in the message.
+ */
+ public function hasHeader($name);
+
+ /**
+ * Retrieves a message header value by the given case-insensitive name.
+ *
+ * This method returns an array of all the header values of the given
+ * case-insensitive header name.
+ *
+ * If the header does not appear in the message, this method MUST return an
+ * empty array.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string[] An array of string values as provided for the given
+ * header. If the header does not appear in the message, this method MUST
+ * return an empty array.
+ */
+ public function getHeader($name);
+
+ /**
+ * Retrieves a comma-separated string of the values for a single header.
+ *
+ * This method returns all of the header values of the given
+ * case-insensitive header name as a string concatenated together using
+ * a comma.
+ *
+ * NOTE: Not all header values may be appropriately represented using
+ * comma concatenation. For such headers, use getHeader() instead
+ * and supply your own delimiter when concatenating.
+ *
+ * If the header does not appear in the message, this method MUST return
+ * an empty string.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string A string of values as provided for the given header
+ * concatenated together using a comma. If the header does not appear in
+ * the message, this method MUST return an empty string.
+ */
+ public function getHeaderLine($name);
+
+ /**
+ * Return an instance with the provided value replacing the specified header.
+ *
+ * While header names are case-insensitive, the casing of the header will
+ * be preserved by this function, and returned from getHeaders().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new and/or updated header and value.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @param string|string[] $value Header value(s).
+ * @return self
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withHeader($name, $value);
+
+ /**
+ * Return an instance with the specified header appended with the given value.
+ *
+ * Existing values for the specified header will be maintained. The new
+ * value(s) will be appended to the existing list. If the header did not
+ * exist previously, it will be added.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new header and/or value.
+ *
+ * @param string $name Case-insensitive header field name to add.
+ * @param string|string[] $value Header value(s).
+ * @return self
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withAddedHeader($name, $value);
+
+ /**
+ * Return an instance without the specified header.
+ *
+ * Header resolution MUST be done without case-sensitivity.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the named header.
+ *
+ * @param string $name Case-insensitive header field name to remove.
+ * @return self
+ */
+ public function withoutHeader($name);
+
+ /**
+ * Gets the body of the message.
+ *
+ * @return StreamInterface Returns the body as a stream.
+ */
+ public function getBody();
+
+ /**
+ * Return an instance with the specified message body.
+ *
+ * The body MUST be a StreamInterface object.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return a new instance that has the
+ * new body stream.
+ *
+ * @param StreamInterface $body Body.
+ * @return self
+ * @throws \InvalidArgumentException When the body is not valid.
+ */
+ public function withBody(StreamInterface $body);
+}
diff --git a/library/vendor/Psr/Http/Message/RequestInterface.php b/library/vendor/Psr/Http/Message/RequestInterface.php
new file mode 100644
index 0000000..75c802e
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/RequestInterface.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an outgoing, client-side request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * During construction, implementations MUST attempt to set the Host header from
+ * a provided URI if no Host header is provided.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface RequestInterface extends MessageInterface
+{
+ /**
+ * Retrieves the message's request target.
+ *
+ * Retrieves the message's request-target either as it will appear (for
+ * clients), as it appeared at request (for servers), or as it was
+ * specified for the instance (see withRequestTarget()).
+ *
+ * In most cases, this will be the origin-form of the composed URI,
+ * unless a value was provided to the concrete implementation (see
+ * withRequestTarget() below).
+ *
+ * If no URI is available, and no request-target has been specifically
+ * provided, this method MUST return the string "/".
+ *
+ * @return string
+ */
+ public function getRequestTarget();
+
+ /**
+ * Return an instance with the specific request-target.
+ *
+ * If the request needs a non-origin-form request-target — e.g., for
+ * specifying an absolute-form, authority-form, or asterisk-form —
+ * this method may be used to create an instance with the specified
+ * request-target, verbatim.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * changed request target.
+ *
+ * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
+ * request-target forms allowed in request messages)
+ * @param mixed $requestTarget
+ * @return self
+ */
+ public function withRequestTarget($requestTarget);
+
+ /**
+ * Retrieves the HTTP method of the request.
+ *
+ * @return string Returns the request method.
+ */
+ public function getMethod();
+
+ /**
+ * Return an instance with the provided HTTP method.
+ *
+ * While HTTP method names are typically all uppercase characters, HTTP
+ * method names are case-sensitive and thus implementations SHOULD NOT
+ * modify the given string.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * changed request method.
+ *
+ * @param string $method Case-sensitive method.
+ * @return self
+ * @throws \InvalidArgumentException for invalid HTTP methods.
+ */
+ public function withMethod($method);
+
+ /**
+ * Retrieves the URI instance.
+ *
+ * This method MUST return a UriInterface instance.
+ *
+ * @link http://tools.ietf.org/html/rfc3986#section-4.3
+ * @return UriInterface Returns a UriInterface instance
+ * representing the URI of the request.
+ */
+ public function getUri();
+
+ /**
+ * Returns an instance with the provided URI.
+ *
+ * This method MUST update the Host header of the returned request by
+ * default if the URI contains a host component. If the URI does not
+ * contain a host component, any pre-existing Host header MUST be carried
+ * over to the returned request.
+ *
+ * You can opt-in to preserving the original state of the Host header by
+ * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+ * `true`, this method interacts with the Host header in the following ways:
+ *
+ * - If the the Host header is missing or empty, and the new URI contains
+ * a host component, this method MUST update the Host header in the returned
+ * request.
+ * - If the Host header is missing or empty, and the new URI does not contain a
+ * host component, this method MUST NOT update the Host header in the returned
+ * request.
+ * - If a Host header is present and non-empty, this method MUST NOT update
+ * the Host header in the returned request.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new UriInterface instance.
+ *
+ * @link http://tools.ietf.org/html/rfc3986#section-4.3
+ * @param UriInterface $uri New request URI to use.
+ * @param bool $preserveHost Preserve the original state of the Host header.
+ * @return self
+ */
+ public function withUri(UriInterface $uri, $preserveHost = false);
+}
diff --git a/library/vendor/Psr/Http/Message/ResponseInterface.php b/library/vendor/Psr/Http/Message/ResponseInterface.php
new file mode 100644
index 0000000..6724809
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/ResponseInterface.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an outgoing, server-side response.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - Status code and reason phrase
+ * - Headers
+ * - Message body
+ *
+ * Responses are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface ResponseInterface extends MessageInterface
+{
+ /**
+ * Gets the response status code.
+ *
+ * The status code is a 3-digit integer result code of the server's attempt
+ * to understand and satisfy the request.
+ *
+ * @return int Status code.
+ */
+ public function getStatusCode();
+
+ /**
+ * Return an instance with the specified status code and, optionally, reason phrase.
+ *
+ * If no reason phrase is specified, implementations MAY choose to default
+ * to the RFC 7231 or IANA recommended reason phrase for the response's
+ * status code.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated status and reason phrase.
+ *
+ * @link http://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @param int $code The 3-digit integer result code to set.
+ * @param string $reasonPhrase The reason phrase to use with the
+ * provided status code; if none is provided, implementations MAY
+ * use the defaults as suggested in the HTTP specification.
+ * @return self
+ * @throws \InvalidArgumentException For invalid status code arguments.
+ */
+ public function withStatus($code, $reasonPhrase = '');
+
+ /**
+ * Gets the response reason phrase associated with the status code.
+ *
+ * Because a reason phrase is not a required element in a response
+ * status line, the reason phrase value MAY be null. Implementations MAY
+ * choose to return the default RFC 7231 recommended reason phrase (or those
+ * listed in the IANA HTTP Status Code Registry) for the response's
+ * status code.
+ *
+ * @link http://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @return string Reason phrase; must return an empty string if none present.
+ */
+ public function getReasonPhrase();
+}
diff --git a/library/vendor/Psr/Http/Message/ServerRequestInterface.php b/library/vendor/Psr/Http/Message/ServerRequestInterface.php
new file mode 100644
index 0000000..916e065
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/ServerRequestInterface.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an incoming, server-side HTTP request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * Additionally, it encapsulates all data as it has arrived to the
+ * application from the CGI and/or PHP environment, including:
+ *
+ * - The values represented in $_SERVER.
+ * - Any cookies provided (generally via $_COOKIE)
+ * - Query string arguments (generally via $_GET, or as parsed via parse_str())
+ * - Upload files, if any (as represented by $_FILES)
+ * - Deserialized body parameters (generally from $_POST)
+ *
+ * $_SERVER values MUST be treated as immutable, as they represent application
+ * state at the time of request; as such, no methods are provided to allow
+ * modification of those values. The other values provide such methods, as they
+ * can be restored from $_SERVER or the request body, and may need treatment
+ * during the application (e.g., body parameters may be deserialized based on
+ * content type).
+ *
+ * Additionally, this interface recognizes the utility of introspecting a
+ * request to derive and match additional parameters (e.g., via URI path
+ * matching, decrypting cookie values, deserializing non-form-encoded body
+ * content, matching authorization headers to users, etc). These parameters
+ * are stored in an "attributes" property.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface ServerRequestInterface extends RequestInterface
+{
+ /**
+ * Retrieve server parameters.
+ *
+ * Retrieves data related to the incoming request environment,
+ * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+ * REQUIRED to originate from $_SERVER.
+ *
+ * @return array
+ */
+ public function getServerParams();
+
+ /**
+ * Retrieve cookies.
+ *
+ * Retrieves cookies sent by the client to the server.
+ *
+ * The data MUST be compatible with the structure of the $_COOKIE
+ * superglobal.
+ *
+ * @return array
+ */
+ public function getCookieParams();
+
+ /**
+ * Return an instance with the specified cookies.
+ *
+ * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+ * be compatible with the structure of $_COOKIE. Typically, this data will
+ * be injected at instantiation.
+ *
+ * This method MUST NOT update the related Cookie header of the request
+ * instance, nor related values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated cookie values.
+ *
+ * @param array $cookies Array of key/value pairs representing cookies.
+ * @return self
+ */
+ public function withCookieParams(array $cookies);
+
+ /**
+ * Retrieve query string arguments.
+ *
+ * Retrieves the deserialized query string arguments, if any.
+ *
+ * Note: the query params might not be in sync with the URI or server
+ * params. If you need to ensure you are only getting the original
+ * values, you may need to parse the query string from `getUri()->getQuery()`
+ * or from the `QUERY_STRING` server param.
+ *
+ * @return array
+ */
+ public function getQueryParams();
+
+ /**
+ * Return an instance with the specified query string arguments.
+ *
+ * These values SHOULD remain immutable over the course of the incoming
+ * request. They MAY be injected during instantiation, such as from PHP's
+ * $_GET superglobal, or MAY be derived from some other value such as the
+ * URI. In cases where the arguments are parsed from the URI, the data
+ * MUST be compatible with what PHP's parse_str() would return for
+ * purposes of how duplicate query parameters are handled, and how nested
+ * sets are handled.
+ *
+ * Setting query string arguments MUST NOT change the URI stored by the
+ * request, nor the values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated query string arguments.
+ *
+ * @param array $query Array of query string arguments, typically from
+ * $_GET.
+ * @return self
+ */
+ public function withQueryParams(array $query);
+
+ /**
+ * Retrieve normalized file upload data.
+ *
+ * This method returns upload metadata in a normalized tree, with each leaf
+ * an instance of Psr\Http\Message\UploadedFileInterface.
+ *
+ * These values MAY be prepared from $_FILES or the message body during
+ * instantiation, or MAY be injected via withUploadedFiles().
+ *
+ * @return array An array tree of UploadedFileInterface instances; an empty
+ * array MUST be returned if no data is present.
+ */
+ public function getUploadedFiles();
+
+ /**
+ * Create a new instance with the specified uploaded files.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param array An array tree of UploadedFileInterface instances.
+ * @return self
+ * @throws \InvalidArgumentException if an invalid structure is provided.
+ */
+ public function withUploadedFiles(array $uploadedFiles);
+
+ /**
+ * Retrieve any parameters provided in the request body.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, this method MUST
+ * return the contents of $_POST.
+ *
+ * Otherwise, this method may return any results of deserializing
+ * the request body content; as parsing returns structured content, the
+ * potential types MUST be arrays or objects only. A null value indicates
+ * the absence of body content.
+ *
+ * @return null|array|object The deserialized body parameters, if any.
+ * These will typically be an array or object.
+ */
+ public function getParsedBody();
+
+ /**
+ * Return an instance with the specified body parameters.
+ *
+ * These MAY be injected during instantiation.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, use this method
+ * ONLY to inject the contents of $_POST.
+ *
+ * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+ * deserializing the request body content. Deserialization/parsing returns
+ * structured data, and, as such, this method ONLY accepts arrays or objects,
+ * or a null value if nothing was available to parse.
+ *
+ * As an example, if content negotiation determines that the request data
+ * is a JSON payload, this method could be used to create a request
+ * instance with the deserialized parameters.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param null|array|object $data The deserialized body data. This will
+ * typically be in an array or object.
+ * @return self
+ * @throws \InvalidArgumentException if an unsupported argument type is
+ * provided.
+ */
+ public function withParsedBody($data);
+
+ /**
+ * Retrieve attributes derived from the request.
+ *
+ * The request "attributes" may be used to allow injection of any
+ * parameters derived from the request: e.g., the results of path
+ * match operations; the results of decrypting cookies; the results of
+ * deserializing non-form-encoded message bodies; etc. Attributes
+ * will be application and request specific, and CAN be mutable.
+ *
+ * @return array Attributes derived from the request.
+ */
+ public function getAttributes();
+
+ /**
+ * Retrieve a single derived request attribute.
+ *
+ * Retrieves a single derived request attribute as described in
+ * getAttributes(). If the attribute has not been previously set, returns
+ * the default value as provided.
+ *
+ * This method obviates the need for a hasAttribute() method, as it allows
+ * specifying a default value to return if the attribute is not found.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $default Default value to return if the attribute does not exist.
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null);
+
+ /**
+ * Return an instance with the specified derived request attribute.
+ *
+ * This method allows setting a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $value The value of the attribute.
+ * @return self
+ */
+ public function withAttribute($name, $value);
+
+ /**
+ * Return an instance that removes the specified derived request attribute.
+ *
+ * This method allows removing a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @return self
+ */
+ public function withoutAttribute($name);
+}
diff --git a/library/vendor/Psr/Http/Message/StreamInterface.php b/library/vendor/Psr/Http/Message/StreamInterface.php
new file mode 100644
index 0000000..f68f391
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/StreamInterface.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Describes a data stream.
+ *
+ * Typically, an instance will wrap a PHP stream; this interface provides
+ * a wrapper around the most common operations, including serialization of
+ * the entire stream to a string.
+ */
+interface StreamInterface
+{
+ /**
+ * Reads all data from the stream into a string, from the beginning to end.
+ *
+ * This method MUST attempt to seek to the beginning of the stream before
+ * reading data and read the stream until the end is reached.
+ *
+ * Warning: This could attempt to load a large amount of data into memory.
+ *
+ * This method MUST NOT raise an exception in order to conform with PHP's
+ * string casting operations.
+ *
+ * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+ * @return string
+ */
+ public function __toString();
+
+ /**
+ * Closes the stream and any underlying resources.
+ *
+ * @return void
+ */
+ public function close();
+
+ /**
+ * Separates any underlying resources from the stream.
+ *
+ * After the stream has been detached, the stream is in an unusable state.
+ *
+ * @return resource|null Underlying PHP stream, if any
+ */
+ public function detach();
+
+ /**
+ * Get the size of the stream if known.
+ *
+ * @return int|null Returns the size in bytes if known, or null if unknown.
+ */
+ public function getSize();
+
+ /**
+ * Returns the current position of the file read/write pointer
+ *
+ * @return int Position of the file pointer
+ * @throws \RuntimeException on error.
+ */
+ public function tell();
+
+ /**
+ * Returns true if the stream is at the end of the stream.
+ *
+ * @return bool
+ */
+ public function eof();
+
+ /**
+ * Returns whether or not the stream is seekable.
+ *
+ * @return bool
+ */
+ public function isSeekable();
+
+ /**
+ * Seek to a position in the stream.
+ *
+ * @link http://www.php.net/manual/en/function.fseek.php
+ * @param int $offset Stream offset
+ * @param int $whence Specifies how the cursor position will be calculated
+ * based on the seek offset. Valid values are identical to the built-in
+ * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
+ * offset bytes SEEK_CUR: Set position to current location plus offset
+ * SEEK_END: Set position to end-of-stream plus offset.
+ * @throws \RuntimeException on failure.
+ */
+ public function seek($offset, $whence = SEEK_SET);
+
+ /**
+ * Seek to the beginning of the stream.
+ *
+ * If the stream is not seekable, this method will raise an exception;
+ * otherwise, it will perform a seek(0).
+ *
+ * @see seek()
+ * @link http://www.php.net/manual/en/function.fseek.php
+ * @throws \RuntimeException on failure.
+ */
+ public function rewind();
+
+ /**
+ * Returns whether or not the stream is writable.
+ *
+ * @return bool
+ */
+ public function isWritable();
+
+ /**
+ * Write data to the stream.
+ *
+ * @param string $string The string that is to be written.
+ * @return int Returns the number of bytes written to the stream.
+ * @throws \RuntimeException on failure.
+ */
+ public function write($string);
+
+ /**
+ * Returns whether or not the stream is readable.
+ *
+ * @return bool
+ */
+ public function isReadable();
+
+ /**
+ * Read data from the stream.
+ *
+ * @param int $length Read up to $length bytes from the object and return
+ * them. Fewer than $length bytes may be returned if underlying stream
+ * call returns fewer bytes.
+ * @return string Returns the data read from the stream, or an empty string
+ * if no bytes are available.
+ * @throws \RuntimeException if an error occurs.
+ */
+ public function read($length);
+
+ /**
+ * Returns the remaining contents in a string
+ *
+ * @return string
+ * @throws \RuntimeException if unable to read or an error occurs while
+ * reading.
+ */
+ public function getContents();
+
+ /**
+ * Get stream metadata as an associative array or retrieve a specific key.
+ *
+ * The keys returned are identical to the keys returned from PHP's
+ * stream_get_meta_data() function.
+ *
+ * @link http://php.net/manual/en/function.stream-get-meta-data.php
+ * @param string $key Specific metadata to retrieve.
+ * @return array|mixed|null Returns an associative array if no key is
+ * provided. Returns a specific key value if a key is provided and the
+ * value is found, or null if the key is not found.
+ */
+ public function getMetadata($key = null);
+}
diff --git a/library/vendor/Psr/Http/Message/UploadedFileInterface.php b/library/vendor/Psr/Http/Message/UploadedFileInterface.php
new file mode 100644
index 0000000..5ad288d
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/UploadedFileInterface.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Value object representing a file uploaded through an HTTP request.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ */
+interface UploadedFileInterface
+{
+ /**
+ * Retrieve a stream representing the uploaded file.
+ *
+ * This method MUST return a StreamInterface instance, representing the
+ * uploaded file. The purpose of this method is to allow utilizing native PHP
+ * stream functionality to manipulate the file upload, such as
+ * stream_copy_to_stream() (though the result will need to be decorated in a
+ * native PHP stream wrapper to work with such functions).
+ *
+ * If the moveTo() method has been called previously, this method MUST raise
+ * an exception.
+ *
+ * @return StreamInterface Stream representation of the uploaded file.
+ * @throws \RuntimeException in cases when no stream is available or can be
+ * created.
+ */
+ public function getStream();
+
+ /**
+ * Move the uploaded file to a new location.
+ *
+ * Use this method as an alternative to move_uploaded_file(). This method is
+ * guaranteed to work in both SAPI and non-SAPI environments.
+ * Implementations must determine which environment they are in, and use the
+ * appropriate method (move_uploaded_file(), rename(), or a stream
+ * operation) to perform the operation.
+ *
+ * $targetPath may be an absolute path, or a relative path. If it is a
+ * relative path, resolution should be the same as used by PHP's rename()
+ * function.
+ *
+ * The original file or stream MUST be removed on completion.
+ *
+ * If this method is called more than once, any subsequent calls MUST raise
+ * an exception.
+ *
+ * When used in an SAPI environment where $_FILES is populated, when writing
+ * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+ * used to ensure permissions and upload status are verified correctly.
+ *
+ * If you wish to move to a stream, use getStream(), as SAPI operations
+ * cannot guarantee writing to stream destinations.
+ *
+ * @see http://php.net/is_uploaded_file
+ * @see http://php.net/move_uploaded_file
+ * @param string $targetPath Path to which to move the uploaded file.
+ * @throws \InvalidArgumentException if the $path specified is invalid.
+ * @throws \RuntimeException on any error during the move operation, or on
+ * the second or subsequent call to the method.
+ */
+ public function moveTo($targetPath);
+
+ /**
+ * Retrieve the file size.
+ *
+ * Implementations SHOULD return the value stored in the "size" key of
+ * the file in the $_FILES array if available, as PHP calculates this based
+ * on the actual size transmitted.
+ *
+ * @return int|null The file size in bytes or null if unknown.
+ */
+ public function getSize();
+
+ /**
+ * Retrieve the error associated with the uploaded file.
+ *
+ * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+ *
+ * If the file was uploaded successfully, this method MUST return
+ * UPLOAD_ERR_OK.
+ *
+ * Implementations SHOULD return the value stored in the "error" key of
+ * the file in the $_FILES array.
+ *
+ * @see http://php.net/manual/en/features.file-upload.errors.php
+ * @return int One of PHP's UPLOAD_ERR_XXX constants.
+ */
+ public function getError();
+
+ /**
+ * Retrieve the filename sent by the client.
+ *
+ * Do not trust the value returned by this method. A client could send
+ * a malicious filename with the intention to corrupt or hack your
+ * application.
+ *
+ * Implementations SHOULD return the value stored in the "name" key of
+ * the file in the $_FILES array.
+ *
+ * @return string|null The filename sent by the client or null if none
+ * was provided.
+ */
+ public function getClientFilename();
+
+ /**
+ * Retrieve the media type sent by the client.
+ *
+ * Do not trust the value returned by this method. A client could send
+ * a malicious media type with the intention to corrupt or hack your
+ * application.
+ *
+ * Implementations SHOULD return the value stored in the "type" key of
+ * the file in the $_FILES array.
+ *
+ * @return string|null The media type sent by the client or null if none
+ * was provided.
+ */
+ public function getClientMediaType();
+}
diff --git a/library/vendor/Psr/Http/Message/UriInterface.php b/library/vendor/Psr/Http/Message/UriInterface.php
new file mode 100644
index 0000000..c51a64b
--- /dev/null
+++ b/library/vendor/Psr/Http/Message/UriInterface.php
@@ -0,0 +1,324 @@
+<?php
+
+namespace Psr\Http\Message;
+
+/**
+ * Value object representing a URI.
+ *
+ * This interface is meant to represent URIs according to RFC 3986 and to
+ * provide methods for most common operations. Additional functionality for
+ * working with URIs can be provided on top of the interface or externally.
+ * Its primary use is for HTTP requests, but may also be used in other
+ * contexts.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ *
+ * Typically the Host header will be also be present in the request message.
+ * For server-side requests, the scheme will typically be discoverable in the
+ * server parameters.
+ *
+ * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
+ */
+interface UriInterface
+{
+ /**
+ * Retrieve the scheme component of the URI.
+ *
+ * If no scheme is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.1.
+ *
+ * The trailing ":" character is not part of the scheme and MUST NOT be
+ * added.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.1
+ * @return string The URI scheme.
+ */
+ public function getScheme();
+
+ /**
+ * Retrieve the authority component of the URI.
+ *
+ * If no authority information is present, this method MUST return an empty
+ * string.
+ *
+ * The authority syntax of the URI is:
+ *
+ * <pre>
+ * [user-info@]host[:port]
+ * </pre>
+ *
+ * If the port component is not set or is the standard port for the current
+ * scheme, it SHOULD NOT be included.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.2
+ * @return string The URI authority, in "[user-info@]host[:port]" format.
+ */
+ public function getAuthority();
+
+ /**
+ * Retrieve the user information component of the URI.
+ *
+ * If no user information is present, this method MUST return an empty
+ * string.
+ *
+ * If a user is present in the URI, this will return that value;
+ * additionally, if the password is also present, it will be appended to the
+ * user value, with a colon (":") separating the values.
+ *
+ * The trailing "@" character is not part of the user information and MUST
+ * NOT be added.
+ *
+ * @return string The URI user information, in "username[:password]" format.
+ */
+ public function getUserInfo();
+
+ /**
+ * Retrieve the host component of the URI.
+ *
+ * If no host is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.2.2.
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+ * @return string The URI host.
+ */
+ public function getHost();
+
+ /**
+ * Retrieve the port component of the URI.
+ *
+ * If a port is present, and it is non-standard for the current scheme,
+ * this method MUST return it as an integer. If the port is the standard port
+ * used with the current scheme, this method SHOULD return null.
+ *
+ * If no port is present, and no scheme is present, this method MUST return
+ * a null value.
+ *
+ * If no port is present, but a scheme is present, this method MAY return
+ * the standard port for that scheme, but SHOULD return null.
+ *
+ * @return null|int The URI port.
+ */
+ public function getPort();
+
+ /**
+ * Retrieve the path component of the URI.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * Normally, the empty path "" and absolute path "/" are considered equal as
+ * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+ * do this normalization because in contexts with a trimmed base path, e.g.
+ * the front controller, this difference becomes significant. It's the task
+ * of the user to handle both "" and "/".
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.3.
+ *
+ * As an example, if the value should include a slash ("/") not intended as
+ * delimiter between path segments, that value MUST be passed in encoded
+ * form (e.g., "%2F") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.3
+ * @return string The URI path.
+ */
+ public function getPath();
+
+ /**
+ * Retrieve the query string of the URI.
+ *
+ * If no query string is present, this method MUST return an empty string.
+ *
+ * The leading "?" character is not part of the query and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.4.
+ *
+ * As an example, if a value in a key/value pair of the query string should
+ * include an ampersand ("&") not intended as a delimiter between values,
+ * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.4
+ * @return string The URI query string.
+ */
+ public function getQuery();
+
+ /**
+ * Retrieve the fragment component of the URI.
+ *
+ * If no fragment is present, this method MUST return an empty string.
+ *
+ * The leading "#" character is not part of the fragment and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.5.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.5
+ * @return string The URI fragment.
+ */
+ public function getFragment();
+
+ /**
+ * Return an instance with the specified scheme.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified scheme.
+ *
+ * Implementations MUST support the schemes "http" and "https" case
+ * insensitively, and MAY accommodate other schemes if required.
+ *
+ * An empty scheme is equivalent to removing the scheme.
+ *
+ * @param string $scheme The scheme to use with the new instance.
+ * @return self A new instance with the specified scheme.
+ * @throws \InvalidArgumentException for invalid or unsupported schemes.
+ */
+ public function withScheme($scheme);
+
+ /**
+ * Return an instance with the specified user information.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified user information.
+ *
+ * Password is optional, but the user information MUST include the
+ * user; an empty string for the user is equivalent to removing user
+ * information.
+ *
+ * @param string $user The user name to use for authority.
+ * @param null|string $password The password associated with $user.
+ * @return self A new instance with the specified user information.
+ */
+ public function withUserInfo($user, $password = null);
+
+ /**
+ * Return an instance with the specified host.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified host.
+ *
+ * An empty host value is equivalent to removing the host.
+ *
+ * @param string $host The hostname to use with the new instance.
+ * @return self A new instance with the specified host.
+ * @throws \InvalidArgumentException for invalid hostnames.
+ */
+ public function withHost($host);
+
+ /**
+ * Return an instance with the specified port.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified port.
+ *
+ * Implementations MUST raise an exception for ports outside the
+ * established TCP and UDP port ranges.
+ *
+ * A null value provided for the port is equivalent to removing the port
+ * information.
+ *
+ * @param null|int $port The port to use with the new instance; a null value
+ * removes the port information.
+ * @return self A new instance with the specified port.
+ * @throws \InvalidArgumentException for invalid ports.
+ */
+ public function withPort($port);
+
+ /**
+ * Return an instance with the specified path.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified path.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * If the path is intended to be domain-relative rather than path relative then
+ * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+ * are assumed to be relative to some base path known to the application or
+ * consumer.
+ *
+ * Users can provide both encoded and decoded path characters.
+ * Implementations ensure the correct encoding as outlined in getPath().
+ *
+ * @param string $path The path to use with the new instance.
+ * @return self A new instance with the specified path.
+ * @throws \InvalidArgumentException for invalid paths.
+ */
+ public function withPath($path);
+
+ /**
+ * Return an instance with the specified query string.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified query string.
+ *
+ * Users can provide both encoded and decoded query characters.
+ * Implementations ensure the correct encoding as outlined in getQuery().
+ *
+ * An empty query string value is equivalent to removing the query string.
+ *
+ * @param string $query The query string to use with the new instance.
+ * @return self A new instance with the specified query string.
+ * @throws \InvalidArgumentException for invalid query strings.
+ */
+ public function withQuery($query);
+
+ /**
+ * Return an instance with the specified URI fragment.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified URI fragment.
+ *
+ * Users can provide both encoded and decoded fragment characters.
+ * Implementations ensure the correct encoding as outlined in getFragment().
+ *
+ * An empty fragment value is equivalent to removing the fragment.
+ *
+ * @param string $fragment The fragment to use with the new instance.
+ * @return self A new instance with the specified fragment.
+ */
+ public function withFragment($fragment);
+
+ /**
+ * Return the string representation as a URI reference.
+ *
+ * Depending on which components of the URI are present, the resulting
+ * string is either a full URI or relative reference according to RFC 3986,
+ * Section 4.1. The method concatenates the various components of the URI,
+ * using the appropriate delimiters:
+ *
+ * - If a scheme is present, it MUST be suffixed by ":".
+ * - If an authority is present, it MUST be prefixed by "//".
+ * - The path can be concatenated without delimiters. But there are two
+ * cases where the path has to be adjusted to make the URI reference
+ * valid as PHP does not allow to throw an exception in __toString():
+ * - If the path is rootless and an authority is present, the path MUST
+ * be prefixed by "/".
+ * - If the path is starting with more than one "/" and no authority is
+ * present, the starting slashes MUST be reduced to one.
+ * - If a query is present, it MUST be prefixed by "?".
+ * - If a fragment is present, it MUST be prefixed by "#".
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-4.1
+ * @return string
+ */
+ public function __toString();
+}
diff --git a/library/vendor/Psr/LICENSE b/library/vendor/Psr/LICENSE
new file mode 100644
index 0000000..c2d8e45
--- /dev/null
+++ b/library/vendor/Psr/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014 PHP Framework Interoperability Group
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/library/vendor/Psr/Loader.php b/library/vendor/Psr/Loader.php
new file mode 100644
index 0000000..45c78af
--- /dev/null
+++ b/library/vendor/Psr/Loader.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 Elasticsearch Module (c) 2017 Icinga Development Team | GPLv2+ */
+
+spl_autoload_register(function ($class) {
+ $prefix = 'Psr\\';
+ $len = strlen($prefix);
+
+ $baseDir = __DIR__ . '/';
+
+ if (strncmp($prefix, $class, $len) !== 0) {
+ return;
+ }
+
+ $relative = substr($class, $len);
+
+ $file = $baseDir . str_replace('\\', '/', $relative) . '.php';
+
+ if (file_exists($file)) {
+ require $file;
+ }
+});
diff --git a/library/vendor/iplx/Http/Client.php b/library/vendor/iplx/Http/Client.php
new file mode 100644
index 0000000..39d8905
--- /dev/null
+++ b/library/vendor/iplx/Http/Client.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace iplx\Http;
+
+use RuntimeException;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * HTTP client that uses cURL
+ */
+class Client implements ClientInterface
+{
+ /**
+ * Client version
+ *
+ * @var string
+ */
+ const VERSION = '1.0.0';
+
+ /**
+ * Maximum number of internal cURL handles
+ *
+ * @var int
+ */
+ const MAX_HANDLES = 4;
+
+ /**
+ * Internal cURL handles
+ *
+ * @var array
+ */
+ protected $handles = [];
+
+ /**
+ * Return user agent
+ *
+ * @return string
+ */
+ protected function getAgent()
+ {
+ $defaultAgent = 'ipl/' . self::VERSION;
+ $defaultAgent .= ' curl/' . curl_version()['version'];
+ $defaultAgent .= ' PHP/' . PHP_VERSION;
+
+ return $defaultAgent;
+ }
+
+ /**
+ * Create and return a cURL handle based on the given request
+ *
+ * @param RequestInterface $request
+ * @param array $options
+ *
+ * @return Handle
+ *
+ * @throws RuntimeException
+ */
+ protected function createHandle(RequestInterface $request, array $options)
+ {
+ $headers = [];
+ foreach ($request->getHeaders() as $name => $values) {
+ $headers[] = $name . ': ' . implode(', ', $values);
+ }
+
+ $curlOptions = [
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_FAILONERROR => true,
+ CURLOPT_USERAGENT => $this->getAgent()
+ ];
+
+ if (isset($options['curl'])) {
+ $curlOptions += $options['curl'];
+ }
+
+ $curlOptions += [
+ CURLOPT_CUSTOMREQUEST => $request->getMethod(),
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_RETURNTRANSFER => false,
+ CURLOPT_URL => (string) $request->getUri()->withFragment('')
+ ];
+
+ if (! $request->hasHeader('Accept')) {
+ $curlOptions[CURLOPT_HTTPHEADER][] = 'Accept:';
+ }
+
+ if (! $request->hasHeader('Content-Type')) {
+ $curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:';
+ }
+
+ if (! $request->hasHeader('Expect')) {
+ $curlOptions[CURLOPT_HTTPHEADER][] = 'Expect:';
+ }
+
+ if ($request->getBody()->getSize() !== 0) {
+ $curlOptions[CURLOPT_UPLOAD] = true;
+
+ $body = $request->getBody();
+ if ($body->isSeekable()) {
+ $body->seek(0);
+ }
+
+ $curlOptions[CURLOPT_READFUNCTION] = function ($ch, $infile, $length) use ($body) {
+ return $body->read($length);
+ };
+ }
+
+ if ($request->getProtocolVersion()) {
+ $protocolVersion = null;
+ switch ($request->getProtocolVersion()) {
+ case '2.0':
+ if (version_compare(phpversion(), '7.0.7', '<')) {
+ throw new RuntimeException('You need at least PHP 7.0.7 to use HTTP 2.0');
+ }
+ $protocolVersion = CURL_HTTP_VERSION_2;
+ break;
+ case '1.1':
+ $protocolVersion = CURL_HTTP_VERSION_1_1;
+ break;
+ default:
+ $protocolVersion = CURL_HTTP_VERSION_1_0;
+ }
+
+ $curlOptions[CURLOPT_HTTP_VERSION] = $protocolVersion;
+ }
+
+ $handle = new Handle();
+
+ $curlOptions[CURLOPT_HEADERFUNCTION] = function($ch, $header) use ($handle) {
+ $size = strlen($header);
+
+ if (! trim($header) || strpos($header, 'HTTP/') === 0) {
+ return $size;
+ }
+
+ list($key, $value) = explode(': ', $header, 2);
+ $handle->responseHeaders[$key] = rtrim($value, "\r\n");
+
+ return $size;
+ };
+
+ $handle->responseBody = Stream::open();
+
+ $curlOptions[CURLOPT_WRITEFUNCTION] = function ($ch, $string) use ($handle) {
+ return $handle->responseBody->write($string);
+ };
+
+ $ch = ! empty($this->handles) ? array_pop($this->handles) : curl_init();
+
+ curl_setopt_array($ch, $curlOptions);
+
+ $handle->handle = $ch;
+
+ return $handle;
+ }
+
+ /**
+ * Execute a cURL handle and return the response
+ *
+ * @param Handle $handle
+ *
+ * @return ResponseInterface
+ *
+ * @throws RuntimeException
+ */
+ protected function executeHandle(Handle $handle)
+ {
+ $ch = $handle->handle;
+
+ $success = curl_exec($ch);
+
+ if ($success === false) {
+ throw new RuntimeException(curl_error($ch));
+ }
+
+ $response = new Response(
+ curl_getinfo($ch, CURLINFO_HTTP_CODE), $handle->responseHeaders, $handle->responseBody
+ );
+
+ if (count($this->handles) >= self::MAX_HANDLES) {
+ curl_close($ch);
+ } else {
+ curl_reset($ch);
+
+ $this->handles[] = $ch;
+ }
+
+ return $response;
+ }
+
+ public function send(RequestInterface $request, array $options = [])
+ {
+ $handle = $this->createHandle($request, $options);
+
+ $response = $this->executeHandle($handle);
+
+ return $response;
+ }
+}
diff --git a/library/vendor/iplx/Http/ClientInterface.php b/library/vendor/iplx/Http/ClientInterface.php
new file mode 100644
index 0000000..e7765a7
--- /dev/null
+++ b/library/vendor/iplx/Http/ClientInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace iplx\Http;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Interface for HTTP clients which send HTTP requests
+ */
+interface ClientInterface
+{
+ /**
+ * Send a HTTP request
+ *
+ * @param RequestInterface $request Request to send
+ * @param array $options Request options
+ *
+ * @return ResponseInterface The response
+ */
+ public function send(RequestInterface $request, array $options = []);
+}
diff --git a/library/vendor/iplx/Http/Handle.php b/library/vendor/iplx/Http/Handle.php
new file mode 100644
index 0000000..490b5c5
--- /dev/null
+++ b/library/vendor/iplx/Http/Handle.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace iplx\Http;
+
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Internal cURL handle representation
+ */
+class Handle
+{
+ /**
+ * cURL handle
+ *
+ * @var resource
+ */
+ public $handle;
+
+ /**
+ * Response body
+ *
+ * @var StreamInterface
+ */
+ public $responseBody;
+
+ /**
+ * Received response headers
+ *
+ * @var array
+ */
+ public $responseHeaders = [];
+}
diff --git a/library/vendor/iplx/Http/MessageTrait.php b/library/vendor/iplx/Http/MessageTrait.php
new file mode 100644
index 0000000..c8dc9b3
--- /dev/null
+++ b/library/vendor/iplx/Http/MessageTrait.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace iplx\Http;
+
+use Psr\Http\Message\StreamInterface;
+
+trait MessageTrait
+{
+ /**
+ * Case sensitive header names with lowercase header names as keys
+ *
+ * @var array
+ */
+ protected $headerNames = [];
+
+ /**
+ * Header values with lowercase header names as keys
+ *
+ * @var array
+ */
+ protected $headerValues = [];
+
+ /**
+ * The body of this request
+ *
+ * @var StreamInterface
+ */
+ protected $body;
+
+ /**
+ * Protocol version
+ *
+ * @var string
+ */
+ protected $protocolVersion;
+
+ public function getProtocolVersion()
+ {
+ return $this->protocolVersion;
+ }
+
+ public function withProtocolVersion($version)
+ {
+ $message = clone $this;
+ $message->protocolVersion = $version;
+
+ return $message;
+ }
+
+ public function getHeaders()
+ {
+ return array_combine($this->headerNames, $this->headerValues);
+ }
+
+ public function hasHeader($header)
+ {
+ return isset($this->headerValues[strtolower($header)]);
+ }
+
+ public function getHeader($header)
+ {
+ $header = strtolower($header);
+
+ if (! isset($this->headerValues[$header])) {
+ return [];
+ }
+
+ return $this->headerValues[$header];
+ }
+
+ public function getHeaderLine($name)
+ {
+ $name = strtolower($name);
+
+ if (! isset($this->headerValues[$name])) {
+ return '';
+ }
+
+ return implode(', ', $this->headerValues[$name]);
+ }
+
+ public function withHeader($name, $value)
+ {
+ $name = rtrim($name);
+
+ $value = $this->normalizeHeaderValues($value);
+
+ $normalized = strtolower($name);
+
+ $message = clone $this;
+ $message->headerNames[$normalized] = $name;
+ $message->headerValues[$normalized] = $value;
+
+ return $message;
+ }
+
+ public function withAddedHeader($name, $value)
+ {
+ $name = rtrim($name);
+
+ $value = $this->normalizeHeaderValues($value);
+
+ $normalized = strtolower($name);
+
+ $message = clone $this;
+ if (isset($message->headerNames[$normalized])) {
+ $message->headerValues[$normalized] = array_merge($message->headerValues[$normalized], $value);
+ } else {
+ $message->headerNames[$normalized] = $name;
+ $message->headerValues[$normalized] = $value;
+ }
+
+ return $message;
+ }
+
+ public function withoutHeader($name)
+ {
+ $normalized = strtolower(rtrim($name));
+
+ $message = clone $this;
+ unset($message->headerNames[$normalized]);
+ unset($message->headerValues[$normalized]);
+
+ return $message;
+ }
+
+ public function getBody()
+ {
+ return $this->body;
+ }
+
+ public function withBody(StreamInterface $body)
+ {
+ $message = clone $this;
+ $message->body = $body;
+
+ return $message;
+ }
+
+ protected function setHeaders(array $headers)
+ {
+ // Prepare header field names and header field values according to
+ // https://tools.ietf.org/html/rfc7230#section-3.2.4
+ $names = array_map('rtrim', array_keys($headers));
+ $values = $this->normalizeHeaderValues($headers);
+
+ $normalized = array_map('strtolower', $names);
+
+ $this->headerNames = array_combine(
+ $normalized,
+ $names
+ );
+
+ $this->headerValues = array_combine(
+ $normalized,
+ $values
+ );
+ }
+
+ protected function normalizeHeaderValues(array $values)
+ {
+ // Prepare header field names and header field values according to
+ // https://tools.ietf.org/html/rfc7230#section-3.2.4
+ return array_map(function ($value) {
+ if (! is_array($value)) {
+ $value = [$value];
+ }
+
+ return array_map(function ($value) {
+ return trim($value, " \t");
+ }, $value);
+ }, $values);
+ }
+}
diff --git a/library/vendor/iplx/Http/Request.php b/library/vendor/iplx/Http/Request.php
new file mode 100644
index 0000000..b9fae7d
--- /dev/null
+++ b/library/vendor/iplx/Http/Request.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace iplx\Http;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\UriInterface;
+
+/**
+ * A HTTP request
+ */
+class Request implements RequestInterface
+{
+ use MessageTrait;
+
+ /**
+ * HTTP method of the request
+ *
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * The request target
+ *
+ * @var string|null
+ */
+ protected $requestTarget;
+
+ /**
+ * URI of the request
+ *
+ * @var UriInterface
+ */
+ protected $uri;
+
+ /**
+ * Create a new HTTP request
+ *
+ * @param string $method HTTP method
+ * @param string $uri URI
+ * @param array $headers Request headers
+ * @param string $body Request body
+ * @param string $protocolVersion Protocol version
+ */
+ public function __construct($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1')
+ {
+ $this->method = $method;
+ $this->uri = new Uri($uri);
+ $this->setHeaders($headers);
+ $this->body = Stream::create($body);
+ $this->protocolVersion = $protocolVersion;
+
+ $this->provideHostHeader();
+ }
+
+ public function getRequestTarget()
+ {
+ if ($this->requestTarget !== null) {
+ return $this->requestTarget;
+ }
+
+ $requestTarget = $this->uri->getPath();
+
+ // Weak type checks to also check null
+
+ if ($requestTarget == '') {
+ $requestTarget = '/';
+ }
+
+ if ($this->uri->getQuery() != '') {
+ $requestTarget .= '?' . $this->uri->getQuery();
+ }
+
+ return $requestTarget;
+ }
+
+ public function withRequestTarget($requestTarget)
+ {
+ $request = clone $this;
+ $request->requestTarget = $requestTarget;
+
+ return $request;
+ }
+
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ public function withMethod($method)
+ {
+ $request = clone $this;
+ $request->method = $method;
+
+ return $this;
+ }
+
+ public function getUri()
+ {
+ return $this->uri;
+ }
+
+ public function withUri(UriInterface $uri, $preserveHost = false)
+ {
+ $request = clone $this;
+ $request->uri = $uri;
+
+ if (! $preserveHost) {
+ $this->provideHostHeader(true);
+ }
+
+ return $this;
+ }
+
+ protected function provideHostHeader($force = false)
+ {
+ if ($this->hasHeader('host')) {
+ if (! $force) {
+ return;
+ }
+
+ $header = $this->headerNames['host'];
+ } else {
+ $header = 'Host';
+ }
+
+ $host = $this->uri->getHost();
+
+ // Weak type check to also check null
+ if ($host == '') {
+ $host = '';
+ } else {
+ $port = $this->uri->getPort();
+
+ if ($port !== null) {
+ $host .= ":$port";
+ }
+ }
+
+ $this->headerNames['host'] = $header;
+ $this->headerValues['host'] = [$host];
+ }
+}
diff --git a/library/vendor/iplx/Http/Response.php b/library/vendor/iplx/Http/Response.php
new file mode 100644
index 0000000..25448b1
--- /dev/null
+++ b/library/vendor/iplx/Http/Response.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace iplx\Http;
+
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * A HTTP response
+ */
+class Response implements ResponseInterface
+{
+ use MessageTrait;
+
+ /**
+ * Status code of the response
+ *
+ * @var int
+ */
+ protected $statusCode;
+
+ /**
+ * Response status reason phrase
+ *
+ * @var string
+ */
+ protected $reasonPhrase = '';
+
+ /**
+ * Create a new HTTP response
+ *
+ * @param int $statusCode Response status code
+ * @param array $headers Response headers
+ * @param string $body Response body
+ * @param string $protocolVersion Protocol version
+ * @param string $reasonPhrase Response status reason phrase
+ */
+ public function __construct($statusCode = 200, array $headers = [], $body = null, $protocolVersion = '1.1', $reasonPhrase = '')
+ {
+ $this->statusCode = $statusCode;
+ $this->setHeaders($headers);
+ $this->body = Stream::create($body);
+ $this->protocolVersion = $protocolVersion;
+ $this->reasonPhrase = $reasonPhrase;
+ }
+
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ public function withStatus($code, $reasonPhrase = '')
+ {
+ $response = clone $this;
+ $response->statusCode = $code;
+ $response->reasonPhrase = $reasonPhrase;
+
+ return $response;
+ }
+
+ public function getReasonPhrase()
+ {
+ return $this->reasonPhrase;
+ }
+}
diff --git a/library/vendor/iplx/Http/Stream.php b/library/vendor/iplx/Http/Stream.php
new file mode 100644
index 0000000..a113312
--- /dev/null
+++ b/library/vendor/iplx/Http/Stream.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace iplx\Http;
+
+use Exception;
+use InvalidArgumentException;
+use RuntimeException;
+use Psr\Http\Message\StreamInterface;
+
+class Stream implements StreamInterface
+{
+ protected $stream;
+
+ protected $size;
+
+ protected $seekable;
+
+ protected $readable;
+
+ protected $writable;
+
+ public function __construct($stream)
+ {
+ if (! is_resource($stream)) {
+ throw new InvalidArgumentException('Invalid stream resource');
+ }
+
+ $this->stream = $stream;
+
+ $meta = stream_get_meta_data($this->stream);
+ $this->seekable = $meta['seekable'];
+ $this->readable = preg_match('/[r+]/', $meta['mode']) === 1;
+ $this->writable = preg_match('/[waxc+]/', $meta['mode']) === 1;
+ }
+
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ public function __toString()
+ {
+ try {
+ $this->seek(0);
+ $contents = stream_get_contents($this->stream);
+ } catch (Exception $e) {
+ $contents = '';
+ }
+
+ return $contents;
+ }
+
+ public function close()
+ {
+ if (isset($this->stream)) {
+ if (is_resource($this->stream)) {
+ fclose($this->stream);
+ }
+ $this->detach();
+ }
+ }
+
+ public function detach()
+ {
+ if (! isset($this->stream)) {
+ return null;
+ }
+
+ $stream = $this->stream;
+
+ $this->stream = null;
+ $this->size = null;
+ $this->seekable = false;
+ $this->readable = false;
+ $this->writable = false;
+
+ return $stream;
+ }
+
+ public function getSize()
+ {
+ if ($this->size !== null) {
+ return $this->size;
+ }
+
+ if (! isset($this->stream)) {
+ return null;
+ }
+
+ $stats = fstat($this->stream);
+ $this->size = $stats['size'];
+
+ return $this->size;
+ }
+
+ public function tell()
+ {
+ $this->assertAttached();
+
+ $position = ftell($this->stream);
+
+ if ($position === false) {
+ throw new RuntimeException('Unable to determine stream position');
+ }
+
+ return $position;
+ }
+
+ public function eof()
+ {
+ $this->assertAttached();
+
+ return feof($this->stream);
+ }
+
+ public function isSeekable()
+ {
+ return $this->seekable;
+ }
+
+ public function seek($offset, $whence = SEEK_SET)
+ {
+ $this->assertSeekable();
+
+ if (fseek($this->stream, $offset, $whence) === -1) {
+ throw new RuntimeException('Unable to seek to stream position');
+ }
+ }
+
+ public function rewind()
+ {
+ $this->seek(0);
+ }
+
+ public function isWritable()
+ {
+ return $this->writable;
+ }
+
+ public function write($string)
+ {
+ $this->assertWritable();
+
+ $written = fwrite($this->stream, $string);
+
+ if ($written === false) {
+ throw new RuntimeException('Unable to write to stream');
+ }
+
+ return $written;
+ }
+
+ public function isReadable()
+ {
+ return $this->readable;
+ }
+
+ public function read($length)
+ {
+ $this->assertReadable();
+
+ $data = fread($this->stream, $length);
+
+ if ($data === false) {
+ throw new RuntimeException('Unable to read from stream');
+ }
+
+ return $data;
+ }
+
+ public function getContents()
+ {
+ $this->assertReadable();
+
+ $contents = stream_get_contents($this->stream);
+
+ if ($contents === false) {
+ throw new RuntimeException('Unable to read stream contents');
+ }
+
+ return $contents;
+ }
+
+ public function getMetadata($key = null)
+ {
+ if (! isset($this->stream)) {
+ return $key === null ? [] : null;
+ }
+
+ $meta = stream_get_meta_data($this->stream);
+
+ if ($key === null) {
+ return $meta;
+ }
+
+ if (isset($meta[$key])) {
+ return $meta[$key];
+ }
+
+ return null;
+ }
+
+ public function assertAttached()
+ {
+ if (! isset($this->stream)) {
+ throw new RuntimeException('Stream is detached');
+ }
+ }
+
+ public function assertSeekable()
+ {
+ $this->assertAttached();
+
+ if (! $this->isSeekable()) {
+ throw new RuntimeException('Stream is not seekable');
+ }
+ }
+
+ public function assertReadable()
+ {
+ $this->assertAttached();
+
+ if (! $this->isReadable()) {
+ throw new RuntimeException('Stream is not readable');
+ }
+ }
+
+ public function assertWritable()
+ {
+ $this->assertAttached();
+
+ if (! $this->isWritable()) {
+ throw new RuntimeException('Stream is not writable');
+ }
+ }
+
+ /**
+ * Open a stream
+ *
+ * @param string $filename
+ * @param string $mode
+ *
+ * @return static
+ */
+ public static function open($filename = 'php://temp', $mode = 'r+')
+ {
+ $stream = fopen($filename, $mode);
+
+ return new static($stream);
+ }
+
+ /**
+ * Create a stream
+ *
+ * @param StreamInterface|string|resource $resource
+ *
+ * @return StreamInterface
+ */
+ public static function create($resource)
+ {
+ if ($resource instanceof StreamInterface) {
+ return $resource;
+ }
+
+ if (is_scalar($resource)) {
+ $stream = fopen('php://temp', 'r+');
+
+ if ($resource !== '') {
+ fwrite($stream, $resource);
+ fseek($stream, 0);
+ }
+
+ return new static($stream);
+ }
+
+ if (is_resource($resource)) {
+ return new static($resource);
+ }
+
+ return static::open();
+ }
+
+}
diff --git a/library/vendor/iplx/Http/Uri.php b/library/vendor/iplx/Http/Uri.php
new file mode 100644
index 0000000..044fb17
--- /dev/null
+++ b/library/vendor/iplx/Http/Uri.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace iplx\Http;
+
+use InvalidArgumentException;
+use Psr\Http\Message\UriInterface;
+
+class Uri implements UriInterface
+{
+ protected $scheme;
+
+ protected $host;
+
+ protected $port;
+
+ protected $user;
+
+ protected $pass;
+
+ protected $path;
+
+ protected $query;
+
+ protected $fragment;
+
+ public function __construct($uri = null)
+ {
+ $parts = parse_url($uri);
+
+ if ($parts === false) {
+ throw new InvalidArgumentException();
+ }
+
+ foreach ($parts as $component => $value) {
+ $this->$component = $value;
+ }
+ }
+
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ public function getAuthority()
+ {
+ // Weak type check to also check null
+ if ($this->host == '') {
+ return '';
+ }
+
+ $authority = $this->host;
+
+ $userInfo = $this->getUserInfo();
+ $port = $this->getPort();
+
+ if ($userInfo) {
+ $authority = "$userInfo@$authority";
+ }
+
+ if ($port !== null) {
+ $authority .= ":$port";
+ }
+
+ return $authority;
+ }
+
+ public function getUserInfo()
+ {
+ $userInfo = $this->user;
+
+ if ($this->pass !== null) {
+ $userInfo .= ":{$this->pass}";
+ }
+
+ return $userInfo;
+ }
+
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ public function getFragment()
+ {
+ return $this->fragment;
+ }
+
+ public function withScheme($scheme)
+ {
+ $uri = clone $this;
+ $uri->scheme = $scheme;
+
+ return $uri;
+ }
+
+ public function withUserInfo($user, $password = null)
+ {
+ $uri = clone $this;
+ $uri->user = $user;
+ $uri->pass = $password;
+
+ return $uri;
+ }
+
+ public function withHost($host)
+ {
+ $uri = clone $this;
+ $uri->host = $host;
+
+ return $uri;
+ }
+
+ public function withPort($port)
+ {
+ $uri = clone $this;
+ $uri->port = $port;
+
+ return $uri;
+ }
+
+ public function withPath($path)
+ {
+ $uri = clone $this;
+ $uri->path = $path;
+
+ return $uri;
+ }
+
+ public function withQuery($query)
+ {
+ $uri = clone $this;
+ $uri->query = $query;
+
+ return $uri;
+ }
+
+ public function withFragment($fragment)
+ {
+ $uri = clone $this;
+ $uri->fragment = $fragment;
+
+ return $uri;
+ }
+
+ public function __toString()
+ {
+ $scheme = $this->getScheme();
+ $authority = $this->getAuthority();
+ $path = $this->getPath();
+ $query = $this->getQuery();
+ $fragment = $this->getFragment();
+
+ $uri = '';
+
+ // Weak type checks to also check null
+
+ if ($scheme != '') {
+ $uri = "$scheme:";
+ }
+
+ if ($authority != '') {
+ $uri .= "//$authority";
+ }
+
+ if ($path != '') {
+ if ($path[0] === '/') {
+ if ($authority == '') {
+ $path = ltrim($path, '/');
+ }
+ } else {
+ $path = "/$path";
+ }
+
+ $uri .= $path;
+ }
+
+ if ($query != '') {
+ $uri .= "?$query";
+ }
+
+ if ($fragment != '') {
+ $uri .= "#$fragment";
+ }
+
+ return $uri;
+ }
+}
diff --git a/library/vendor/iplx/LICENSE b/library/vendor/iplx/LICENSE
new file mode 100644
index 0000000..ecbc059
--- /dev/null
+++ b/library/vendor/iplx/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. \ No newline at end of file
diff --git a/library/vendor/iplx/Loader.php b/library/vendor/iplx/Loader.php
new file mode 100644
index 0000000..5a6c9c0
--- /dev/null
+++ b/library/vendor/iplx/Loader.php
@@ -0,0 +1,20 @@
+<?php
+
+spl_autoload_register(function ($class) {
+ $prefix = 'iplx\\';
+ $len = strlen($prefix);
+
+ $baseDir = __DIR__ . '/';
+
+ if (strncmp($prefix, $class, $len) !== 0) {
+ return;
+ }
+
+ $relative = substr($class, $len);
+
+ $file = $baseDir . str_replace('\\', '/', $relative) . '.php';
+
+ if (file_exists($file)) {
+ require $file;
+ }
+});
diff --git a/library/vendor/iplx/README b/library/vendor/iplx/README
new file mode 100644
index 0000000..aa0b11e
--- /dev/null
+++ b/library/vendor/iplx/README
@@ -0,0 +1 @@
+Experimental version of the not released Icinga PHP Library (ipl). Do not use.
diff --git a/module.info b/module.info
new file mode 100644
index 0000000..110c851
--- /dev/null
+++ b/module.info
@@ -0,0 +1,8 @@
+Module: graphite
+Version: 1.2.2
+Requires:
+ Libraries: icinga-php-library (>=0.9.0), icinga-php-thirdparty (>=0.10.0)
+ Modules: monitoring (>=2.9.0), icingadb (>=1.0.0)
+Description: Icinga Web Graphite Integration
+ This module integrates an existing Graphite installation in your Icinga Web
+ frontend.
diff --git a/public/css/module.less b/public/css/module.less
new file mode 100644
index 0000000..bb651e7
--- /dev/null
+++ b/public/css/module.less
@@ -0,0 +1,213 @@
+div.images {
+ h3 {
+ clear: both;
+ }
+
+ img.svg {
+ float: left;
+ border: none;
+ }
+
+}
+
+div.images.object-detail-view {
+ display: block;
+
+ img.graphiteImg {
+ width: 100%;
+ display: block;
+ }
+}
+
+.timerangepicker-container {
+ display: flex;
+ padding: .25em 0;
+
+ .button-link {
+ line-height: 1.75;
+ width: 2.25em;
+
+ i {
+ display: inline-block;
+ width: 100%;
+ text-align: center;
+ }
+
+ i:before {
+ margin-right: 0;
+ }
+ }
+}
+
+form[name=form_timerangepickercommon_graphite] {
+ display: inline-flex;
+ flex: 1 1 auto;
+ width: 0;
+ max-width: 43.25em;
+
+ &:not(:last-child) {
+ margin-right: .25em;
+ }
+
+ select {
+ min-width: 3.5em;
+ max-width: 7em;
+ flex: 1 1 auto;
+ width: auto;
+
+ &:not(:last-of-type) {
+ margin-right: .25em;
+ }
+ }
+}
+
+form[name=form_timerangepickercustom_graphite] {
+ min-width: 21em;
+
+ .control-label-group {
+ text-align: left;
+ }
+
+ input[type=date] {
+ min-width: 11em;
+ }
+
+ input[type=time] {
+ min-width: 5em;
+ }
+}
+
+.flyover[id^="graphite-customrange-"] {
+ display: inline-block;
+}
+
+.grid {
+ margin-right: -1em;
+ margin-bottom: -1em;
+
+ .empty-state {
+ .rounded-corners();
+ background-color: @gray-lightest;
+ margin-right: 1em;
+ padding: 1em;
+ text-align: center;
+ }
+}
+
+.grid-item {
+ display: inline-block;
+ border-radius: .25em;
+ border: 1px solid @gray-lighter;
+ min-width: 302px;
+ vertical-align: top;
+ margin-right: 1em;
+ margin-bottom: 1em;
+}
+
+.grid-item h2 {
+ margin-top: 0;
+ background: @gray-lightest;
+ border-bottom: 1px solid @gray-lighter;
+ padding:.25em .5em;
+ max-width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.grid-item > .images:not(:last-child) {
+ margin-right: 1em;
+ display: inline-block;
+}
+
+.grid-item .graphiteImg {
+ margin-right: 1px;
+}
+
+.grid-item > p {
+ padding: .5em;
+ margin-bottom: 0;
+}
+
+p.load-more {
+ margin-right: 1em;
+ text-align: right;
+}
+
+// TODO: Remove once ipl-web v0.7.0 is required
+.controls:not(.default-layout) {
+ .box-shadow(0, 0, 0, 1px, @gray-lighter);
+
+ .pagination-control {
+ float: left;
+ }
+
+ .sort-control,
+ .limit-control {
+ float: right;
+ margin-left: .5em;
+ }
+
+ .sort-control {
+ display: flex;
+ justify-content: flex-end;
+
+ :not(.form-element) > label {
+ margin-right: 0;
+ }
+
+ .control-button {
+ margin: 0;
+ }
+ }
+
+ > :not(:only-child) {
+ margin-bottom: 0.5em;
+ }
+
+ .search-suggestions {
+ margin-bottom: 2.5em;
+ }
+
+ .search-controls,
+ .filter {
+ clear: both;
+ display: flex;
+ min-width: 100%;
+
+ .search-bar {
+ flex: 1 1 auto;
+
+ & ~ .control-button:last-child {
+ margin-right: -.5em;
+ }
+
+ & ~ .control-button {
+ margin-left: .5em;
+ }
+ }
+ }
+}
+
+/* Graph colors */
+
+@graphite-graph-bg-color: @body-bg-color;
+@graphite-graph-fg-color: @text-color;
+@graphite-graph-major-grid-color: @gray-light;
+@graphite-graph-minor-grid-color: @graphite-graph-bg-color;
+
+@light-mode: {
+ --graphite-graph-bg-color: var(--body-bg-color);
+ --graphite-graph-fg-color: var(--text-color);
+ --graphite-graph-major-grid-color: var(--gray-light);
+ --graphite-graph-minor-grid-color: var(--graphite-graph-bg-color);
+};
+
+.graphite-graph-color-registry {
+ display: none;
+
+ background-color: @graphite-graph-bg-color;
+ color: @graphite-graph-fg-color;
+ border-top-color: @graphite-graph-major-grid-color;
+ border-bottom-color: @graphite-graph-minor-grid-color;
+}
diff --git a/public/js/module.js b/public/js/module.js
new file mode 100644
index 0000000..a2a32f2
--- /dev/null
+++ b/public/js/module.js
@@ -0,0 +1,112 @@
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+(function(Icinga) {
+
+ "use strict";
+
+ class Graphite extends Icinga.EventListener {
+ constructor(icinga) {
+ super(icinga);
+
+ this._colorParams = null;
+ this._resizeTimer = null;
+ this._onResizeBound = this.onResize.bind(this);
+ this._onModeChangeBound = this.onModeChange.bind(this);
+ this._mediaQueryList = window.matchMedia('(prefers-color-scheme: light)');
+
+ this.on('css-reloaded', 'head', this.onCssReloaded, this);
+ this.on('rendered', '#main > .container', this.onRendered, this);
+ window.addEventListener('resize', this._onResizeBound, { passive: true });
+ this._mediaQueryList.addEventListener('change', this._onModeChangeBound, { passive: true });
+ }
+
+ get colorParams() {
+ if (this._colorParams === null) {
+ let colorRegistry = document.querySelector('.graphite-graph-color-registry');
+ let registryStyle = window.getComputedStyle(colorRegistry);
+
+ this._colorParams = {
+ bgcolor: this.rgbToHex(registryStyle.backgroundColor, 'black'),
+ fgcolor: this.rgbToHex(registryStyle.color, 'white'),
+ majorGridLineColor: this.rgbToHex(registryStyle.borderTopColor, '0000003F'),
+ minorGridLineColor: this.rgbToHex(registryStyle.borderBottomColor, 'black')
+ };
+ }
+
+ return this._colorParams;
+ }
+
+ unbind(emitter) {
+ super.unbind(emitter);
+
+ window.removeEventListener('resize', this._onResizeBound);
+ this._mediaQueryList.removeEventListener('change', this._onModeChangeBound);
+
+ this._onResizeBound = null;
+ this._onModeChangeBound = null;
+ this._mediaQueryList = null;
+ }
+
+ onCssReloaded(event) {
+ let _this = event.data.self;
+
+ _this._colorParams = null;
+ _this.updateImages(document);
+ }
+
+ onRendered(event, autorefresh, scripted, autosubmit) {
+ let _this = event.data.self;
+ let container = event.target;
+
+ _this.updateImages(container);
+ }
+
+ onResize() {
+ // Images are not updated instantly, the user might not yet be finished resizing the window
+ if (this._resizeTimer !== null) {
+ clearTimeout(this._resizeTimer);
+ }
+
+ this._resizeTimer = setTimeout(() => this.updateImages(document), 200);
+ }
+
+ onModeChange() {
+ this._colorParams = null;
+ this.updateImages(document);
+ }
+
+ updateImages(container) {
+ container.querySelectorAll('img.graphiteImg[data-actualimageurl]').forEach(img => {
+ let params = { ...this.colorParams }; // Theming ftw!
+ params.r = (new Date()).getTime(); // To bypass the browser cache
+ params.width = img.scrollWidth; // It's either fixed or dependent on parent width
+
+ img.src = this.icinga.utils.addUrlParams(img.dataset.actualimageurl, params);
+ });
+ }
+
+ rgbToHex(rgb, def) {
+ if (! rgb) {
+ return def;
+ }
+
+ let match = rgb.match(/rgba?\((\d+), (\d+), (\d+)(?:, ([\d.]+))?\)/);
+ if (match === null) {
+ return def;
+ }
+
+ let alpha = '';
+ if (typeof match[4] !== 'undefined') {
+ alpha = Math.round(parseFloat(match[4]) * 255).toString(16);
+ }
+
+ return parseInt(match[1], 10).toString(16).padStart(2, '0')
+ + parseInt(match[2], 10).toString(16).padStart(2, '0')
+ + parseInt(match[3], 10).toString(16).padStart(2, '0')
+ + alpha;
+ }
+ }
+
+ Icinga.Behaviors.Graphite = Graphite;
+
+})(Icinga);
diff --git a/run.php b/run.php
new file mode 100644
index 0000000..6c69bc9
--- /dev/null
+++ b/run.php
@@ -0,0 +1,32 @@
+<?php
+
+/** @var \Icinga\Application\Modules\Module $this */
+
+use Icinga\Module\Graphite\ProvidedHook\Icingadb\IcingadbSupport;
+
+require_once $this->getLibDir() . '/vendor/Psr/Loader.php';
+require_once $this->getLibDir() . '/vendor/iplx/Loader.php';
+
+$this->provideHook('monitoring/DetailviewExtension');
+$this->provideHook('icingadb/IcingadbSupport');
+$this->provideHook('icingadb/HostDetailExtension');
+$this->provideHook('icingadb/ServiceDetailExtension');
+
+if (! $this->exists('icingadb') || ! IcingadbSupport::useIcingaDbAsBackend()) {
+ $this->addRoute('graphite/monitoring-graph/host', new Zend_Controller_Router_Route(
+ 'graphite/graph/host',
+ [
+ 'controller' => 'monitoring-graph',
+ 'action' => 'host',
+ 'module' => 'graphite'
+ ]
+ ));
+ $this->addRoute('graphite/monitoring-graph/service', new Zend_Controller_Router_Route(
+ 'graphite/graph/service',
+ [
+ 'controller' => 'monitoring-graph',
+ 'action' => 'service',
+ 'module' => 'graphite'
+ ]
+ ));
+}
diff --git a/templates/cpu_windows_powershell_framework.ini b/templates/cpu_windows_powershell_framework.ini
new file mode 100644
index 0000000..f156851
--- /dev/null
+++ b/templates/cpu_windows_powershell_framework.ini
@@ -0,0 +1,19 @@
+[load-windows.graph]
+check_command = "Invoke-IcingaCheckCPU"
+
+[load-windows.metrics_filters]
+load.value = "$service_name_template$.perfdata.$load$.value"
+crit.value = "$service_name_template$.perfdata.$load$.crit"
+warn.value = "$service_name_template$.perfdata.$load$.warn"
+
+[load-windows.urlparams]
+areaAlpha = "0.5"
+lineWidth = "2"
+min = "0"
+title = "CPU $load$ %"
+yUnitSystem = "none"
+
+[load-windows.functions]
+load.value = "alias(color($metric$, '#1a7dd7'), 'CPU usage(%)')"
+crit.value = "alias(color($metric$, '#ff0000'), 'Crit (%)')"
+warn.value = "alias(color($metric$, '#ff8d00'), 'Warn (%)')"
diff --git a/templates/default.ini b/templates/default.ini
new file mode 100644
index 0000000..5d6921d
--- /dev/null
+++ b/templates/default.ini
@@ -0,0 +1,26 @@
+[default-host.metrics_filters]
+value = "$host_name_template$.perfdata.$perfdata$.value"
+
+[default-host.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+title = "$perfdata$"
+yUnitSystem = "none"
+
+[default-host.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'value')"
+
+
+[default-service.metrics_filters]
+value = "$service_name_template$.perfdata.$perfdata$.value"
+
+[default-service.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+title = "$perfdata$"
+yUnitSystem = "none"
+
+[default-service.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'value')"
diff --git a/templates/disk.ini b/templates/disk.ini
new file mode 100644
index 0000000..f1f97ca
--- /dev/null
+++ b/templates/disk.ini
@@ -0,0 +1,22 @@
+[disk.graph]
+check_command = "disk, disk-windows"
+
+[disk.metrics_filters]
+value = "$service_name_template$.perfdata.$disk$.value"
+max = "$service_name_template$.perfdata.$disk$.max"
+crit = "$service_name_template$.perfdata.$disk$.crit"
+warn = "$service_name_template$.perfdata.$disk$.warn"
+
+[disk.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $disk$"
+yUnitSystem = "binary"
+
+[disk.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
diff --git a/templates/disk_windows_powershell_framework.ini b/templates/disk_windows_powershell_framework.ini
new file mode 100644
index 0000000..1b8e827
--- /dev/null
+++ b/templates/disk_windows_powershell_framework.ini
@@ -0,0 +1,40 @@
+[disk.graph]
+check_command = "Invoke-IcingaCheckUsedPartitionSpace"
+
+[disk.metrics_filters]
+value = "$service_name_template$.perfdata.used_space_$disk$.value"
+max = "$service_name_template$.perfdata.used_space_$disk$.max"
+crit = "$service_name_template$.perfdata.used_space_$disk$.crit"
+warn = "$service_name_template$.perfdata.used_space_$disk$.warn"
+[disk.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $disk$"
+yUnitSystem = "binary"
+
+[disk.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
+
+[disk-percent.graph]
+check_command = "Invoke-IcingaCheckUsedPartitionSpace"
+
+[disk-percent.metrics_filters]
+value = "$service_name_template$.perfdata.$disk$.value"
+crit = "$service_name_template$.perfdata.$disk$.crit"
+warn = "$service_name_template$.perfdata.$disk$.warn"
+[disk-percent.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $disk$"
+
+[disk-percent.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (%)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (%)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (%)')"
diff --git a/templates/ethMon.ini b/templates/ethMon.ini
new file mode 100644
index 0000000..41de344
--- /dev/null
+++ b/templates/ethMon.ini
@@ -0,0 +1,34 @@
+[ethMon-inb.graph]
+check_command = "ethMon"
+
+[ethMon-inb.metrics_filters]
+in.value = "$service_name_template$.perfdata.rx.value"
+
+[ethMon-inb.urlparams]
+areaAlpha = "0.3"
+areaMode = "all"
+lineWidth = "1"
+min = "0"
+title = "Inbound"
+yUnitSystem = "binary"
+
+[ethMon-inb.functions]
+in.value = "alias(color($metric$, '#7CE52D'), 'In (bytes/s)')"
+
+
+[ethMon-out.graph]
+check_command = "ethMon"
+
+[ethMon-out.metrics_filters]
+out.value = "$service_name_template$.perfdata.tx.value"
+
+[ethMon-out.urlparams]
+areaAlpha = "0.3"
+areaMode = "all"
+lineWidth = "1"
+min = "0"
+title = "Outbound"
+yUnitSystem = "binary"
+
+[ethMon-out.functions]
+out.value = "alias(color($metric$, '#ff5566'), 'Out (bytes/s)')"
diff --git a/templates/file_age.ini b/templates/file_age.ini
new file mode 100644
index 0000000..7be72ad
--- /dev/null
+++ b/templates/file_age.ini
@@ -0,0 +1,32 @@
+[file_age-age.graph]
+check_command = "file_age"
+
+[file_age-age.metrics_filters]
+value = "$service_name_template$.perfdata.age.value"
+
+[file_age-age.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[file_age-age.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'File age (s)')"
+
+
+[file_age-size.graph]
+check_command = "file_age"
+
+[file_age-size.metrics_filters]
+value = "$service_name_template$.perfdata.size.value"
+
+[file_age-size.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "binary"
+
+[file_age-size.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'File size (bytes)')"
diff --git a/templates/fping.ini b/templates/fping.ini
new file mode 100644
index 0000000..0b2b41d
--- /dev/null
+++ b/templates/fping.ini
@@ -0,0 +1,32 @@
+[fping-rta.graph]
+check_command = "fping, fping4, fping6"
+
+[fping-rta.metrics_filters]
+rta.value = "$service_name_template$.perfdata.rta.value"
+
+[fping-rta.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[fping-rta.functions]
+rta.value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')"
+
+
+[fping-loss.graph]
+check_command = "fping, fping4, fping6"
+
+[fping-loss.metrics_filters]
+loss.value = "$service_name_template$.perfdata.loss.value"
+
+[fping-loss.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[fping-loss.functions]
+loss.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
diff --git a/templates/graphite-template memory-linux-local.ini b/templates/graphite-template memory-linux-local.ini
new file mode 100644
index 0000000..8868bc0
--- /dev/null
+++ b/templates/graphite-template memory-linux-local.ini
@@ -0,0 +1,28 @@
+[memory.graph]
+check_command = "mem"
+
+[memory.metrics_filters]
+value = "$service_name_template$.perfdata.USED.value"
+max = "$service_name_template$.perfdata.USED.max"
+crit = "$service_name_template$.perfdata.USED.crit"
+warn = "$service_name_template$.perfdata.USED.warn"
+caches = "$service_name_template$.perfdata.CACHES.value"
+free = "$service_name_template$.perfdata.FREE.value"
+total = "$service_name_template$.perfdata.TOTAL.value"
+
+[memory.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Memory USED"
+yUnitSystem = "binary"
+
+[memory.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
+free = "alias(color($metric$, '#41D2A2'), 'Free (bytes)')"
+total = "alias(color($metric$, '#8000FF'), 'Total (bytes)')"
+caches = "alias(color($metric$, '#ABABAB'), 'Caches (bytes)')"
diff --git a/templates/hostalive.ini b/templates/hostalive.ini
new file mode 100644
index 0000000..eda252a
--- /dev/null
+++ b/templates/hostalive.ini
@@ -0,0 +1,32 @@
+[hostalive-rta.graph]
+check_command = "hostalive"
+
+[hostalive-rta.metrics_filters]
+rta.value = "$host_name_template$.perfdata.rta.value"
+
+[hostalive-rta.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[hostalive-rta.functions]
+rta.value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')"
+
+
+[hostalive-pl.graph]
+check_command = "hostalive"
+
+[hostalive-pl.metrics_filters]
+pl.value = "$host_name_template$.perfdata.pl.value"
+
+[hostalive-pl.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[hostalive-pl.functions]
+pl.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
diff --git a/templates/icinga.ini b/templates/icinga.ini
new file mode 100644
index 0000000..b178fb2
--- /dev/null
+++ b/templates/icinga.ini
@@ -0,0 +1,302 @@
+[icinga-uptime.graph]
+check_command = "icinga"
+
+[icinga-uptime.metrics_filters]
+uptime.value = "$service_name_template$.perfdata.uptime.value"
+
+[icinga-uptime.urlparams]
+title = "Uptime"
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[icinga-uptime.functions]
+uptime.value = "alias(color($metric$, '#1a7dd7'), 'Uptime (s)')"
+
+
+[icinga-host-checks.graph]
+check_command = "icinga"
+
+[icinga-host-checks.metrics_filters]
+active_host_checks.value = "$service_name_template$.perfdata.active_host_checks.value"
+passive_host_checks.value = "$service_name_template$.perfdata.passive_host_checks.value"
+
+[icinga-host-checks.urlparams]
+title = "Host Checks"
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-host-checks.functions]
+active_host_checks.value = "alias(color($metric$, '#1a7dd7'), 'Active/s')"
+passive_host_checks.value = "alias(color($metric$, '#0b3c68'), 'Passive/s')"
+
+
+[icinga-service-checks.graph]
+check_command = "icinga"
+
+[icinga-service-checks.metrics_filters]
+active_service_checks.value = "$service_name_template$.perfdata.active_service_checks.value"
+passive_service_checks.value = "$service_name_template$.perfdata.passive_service_checks.value"
+
+[icinga-service-checks.urlparams]
+title = "Service Checks"
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-service-checks.functions]
+active_service_checks.value = "alias(color($metric$, '#1a7dd7'), 'Active/s')"
+passive_service_checks.value = "alias(color($metric$, '#0b3c68'), 'Passive/s')"
+
+
+[icinga-hosts-state.graph]
+check_command = "icinga"
+
+[icinga-hosts-state.metrics_filters]
+num_hosts_up.value = "$service_name_template$.perfdata.num_hosts_up.value"
+num_hosts_down.value = "$service_name_template$.perfdata.num_hosts_down.value"
+num_hosts_unreachable.value = "$service_name_template$.perfdata.num_hosts_unreachable.value"
+num_hosts_pending.value = "$service_name_template$.perfdata.num_hosts_pending.value"
+
+[icinga-hosts-state.urlparams]
+title = "Host States"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-hosts-state.functions]
+num_hosts_up.value = "alias(color($metric$, '#44bb77'), 'Up')"
+num_hosts_down.value = "alias(color($metric$, '#ff5566'), 'Down')"
+num_hosts_unreachable.value = "alias(color($metric$, '#c70fff'), 'Unreachable')"
+num_hosts_pending.value = "alias(color($metric$, '#1a7dd7'), 'Pending')"
+
+
+[icinga-hosts-condition.graph]
+check_command = "icinga"
+
+[icinga-hosts-condition.metrics_filters]
+num_hosts_flapping.value = "$service_name_template$.perfdata.num_hosts_flapping.value"
+num_hosts_in_downtime.value = "$service_name_template$.perfdata.num_hosts_in_downtime.value"
+num_hosts_acknowledged.value = "$service_name_template$.perfdata.num_hosts_acknowledged.value"
+
+[icinga-hosts-condition.urlparams]
+title = "Host Conditions"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-hosts-condition.functions]
+num_hosts_flapping.value = "alias(color($metric$, '#c70fff'), 'Flapping')"
+num_hosts_in_downtime.value = "alias(color($metric$, '#1a7dd7'), 'In Downtime')"
+num_hosts_acknowledged.value = "alias(color($metric$, '#0b3c68'), 'Acknowledged')"
+
+
+[icinga-services-state.graph]
+check_command = "icinga"
+
+[icinga-services-state.metrics_filters]
+num_services_ok.value = "$service_name_template$.perfdata.num_services_ok.value"
+num_services_warning.value = "$service_name_template$.perfdata.num_services_warning.value"
+num_services_critical.value = "$service_name_template$.perfdata.num_services_critical.value"
+num_services_unknown.value = "$service_name_template$.perfdata.num_services_unknown.value"
+num_services_pending.value = "$service_name_template$.perfdata.num_services_pending.value"
+num_services_unreachable.value = "$service_name_template$.perfdata.num_services_unreachable.value"
+
+[icinga-services-state.urlparams]
+title = "Service States"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-services-state.functions]
+num_services_ok.value = "alias(color($metric$, '#44bb77'), 'Ok')"
+num_services_warning.value = "alias(color($metric$, '#ffaa44'), 'Warning')"
+num_services_critical.value = "alias(color($metric$, '#ff5566'), 'Critical')"
+num_services_unknown.value = "alias(color($metric$, '#595959'), 'Unknown')"
+num_services_pending.value = "alias(color($metric$, '#1a7dd7'), 'Pending')"
+num_services_unreachable.value = "alias(color($metric$, '#c70fff'), 'Unreachable')"
+
+
+[icinga-services-condition.graph]
+check_command = "icinga"
+
+[icinga-services-condition.metrics_filters]
+num_services_flapping.value = "$service_name_template$.perfdata.num_services_flapping.value"
+num_services_in_downtime.value = "$service_name_template$.perfdata.num_services_in_downtime.value"
+num_services_acknowledged.value = "$service_name_template$.perfdata.num_services_acknowledged.value"
+
+[icinga-services-condition.urlparams]
+title = "Service Conditions"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-services-condition.functions]
+num_services_flapping.value = "alias(color($metric$, '#c70fff'), 'Flapping')"
+num_services_in_downtime.value = "alias(color($metric$, '#1a7dd7'), 'In Downtime')"
+num_services_acknowledged.value = "alias(color($metric$, '#0b3c68'), 'Acknowledged')"
+
+
+[icinga-latency.graph]
+check_command = "icinga"
+
+[icinga-latency.metrics_filters]
+min_latency.value = "$service_name_template$.perfdata.min_latency.value"
+avg_latency.value = "$service_name_template$.perfdata.avg_latency.value"
+max_latency.value = "$service_name_template$.perfdata.max_latency.value"
+
+[icinga-latency.urlparams]
+title = "Check Latency"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[icinga-latency.functions]
+min_latency.value = "alias(color($metric$, '#61aaed'), 'Min (s)')"
+avg_latency.value = "alias(color($metric$, '#1a7dd7'), 'Avg (s)')"
+max_latency.value = "alias(color($metric$, '#0b3c68'), 'Max (s)')"
+
+
+[icinga-execution-time.graph]
+check_command = "icinga"
+
+[icinga-execution-time.metrics_filters]
+min_execution_time.value = "$service_name_template$.perfdata.min_execution_time.value"
+avg_execution_time.value = "$service_name_template$.perfdata.avg_execution_time.value"
+max_execution_time.value = "$service_name_template$.perfdata.max_execution_time.value"
+
+[icinga-execution-time.urlparams]
+title = "Check Execution Time"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[icinga-execution-time.functions]
+min_execution_time.value = "alias(color($metric$, '#61aaed'), 'Min (s)')"
+avg_execution_time.value = "alias(color($metric$, '#1a7dd7'), 'Avg (s)')"
+max_execution_time.value = "alias(color($metric$, '#0b3c68'), 'Max (s)')"
+
+
+[icinga-api-endpoints.graph]
+check_command = "icinga"
+
+[icinga-api-endpoints.metrics_filters]
+api_num_endpoints.value = "$service_name_template$.perfdata.api_num_endpoints.value"
+api_num_conn_endpoints.value = "$service_name_template$.perfdata.api_num_conn_endpoints.value"
+api_num_not_conn_endpoints.value = "$service_name_template$.perfdata.api_num_not_conn_endpoints.value"
+
+[icinga-api-endpoints.urlparams]
+title = "API Endpoints"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-api-endpoints.functions]
+api_num_endpoints.value = "alias(color($metric$, '#61aaed'), 'All')"
+api_num_conn_endpoints.value = "alias(color($metric$, '#1a7dd7'), 'Connected')"
+api_num_not_conn_endpoints.value = "alias(color($metric$, '#0b3c68'), 'Not Connected')"
+
+
+[icinga-api-http-clients.graph]
+check_command = "icinga"
+
+[icinga-api-http-clients.metrics_filters]
+api_num_http_clients.value = "$service_name_template$.perfdata.api_num_http_clients.value"
+
+[icinga-api-http-clients.urlparams]
+title = "API"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-api-http-clients.functions]
+api_num_http_clients.value = "alias(color($metric$, '#1a7dd7'), 'HTTP Clients')"
+
+
+[icinga-checker.graph]
+check_command = "icinga"
+
+[icinga-checker.metrics_filters]
+checkercomponent_checker_idle.value = "$service_name_template$.perfdata.checkercomponent_checker_idle.value"
+checkercomponent_checker_pending.value = "$service_name_template$.perfdata.checkercomponent_checker_pending.value"
+
+[icinga-checker.urlparams]
+title = "Checker"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-checker.functions]
+checkercomponent_checker_idle.value = "alias(color($metric$, '#61aaed'), 'Idle')"
+checkercomponent_checker_pending.value = "alias(color($metric$, '#1a7dd7'), 'Pending')"
+
+
+[icinga-ido-mysql-queries.graph]
+check_command = "icinga"
+
+[icinga-ido-mysql-queries.metrics_filters]
+idomysqlconnection_ido-mysql_queries_rate.value = "$service_name_template$.perfdata.idomysqlconnection_ido-mysql_queries_rate.value"
+
+[icinga-ido-mysql-queries.urlparams]
+title = "IDO MySQL"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-ido-mysql-queries.functions]
+idomysqlconnection_ido-mysql_queries_rate.value = "alias(color($metric$, '#1a7dd7'), 'Queries/s')"
+
+
+[icinga-ido-mysql-queue.graph]
+check_command = "icinga"
+
+[icinga-ido-mysql-queue.metrics_filters]
+idomysqlconnection_ido-mysql_query_queue_items.value = "$service_name_template$.perfdata.idomysqlconnection_ido-mysql_query_queue_items.value"
+idomysqlconnection_ido-mysql_query_queue_item_rate.value = "$service_name_template$.perfdata.idomysqlconnection_ido-mysql_query_queue_item_rate.value"
+
+[icinga-ido-mysql-queue.urlparams]
+title = "IDO MySQL Queue"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-ido-mysql-queue.functions]
+idomysqlconnection_ido-mysql_query_queue_items.value = "alias(color($metric$, '#1a7dd7'), 'Items')"
+idomysqlconnection_ido-mysql_query_queue_item_rate.value = "alias(color($metric$, '#0b3c68'), 'Items/s')"
+
+
+[icinga-graphite-queue.graph]
+check_command = "icinga"
+
+[icinga-graphite-queue.metrics_filters]
+graphitewriter_graphite_work_queue_items.value = "$service_name_template$.perfdata.graphitewriter_graphite_work_queue_items.value"
+graphitewriter_graphite_work_queue_item_rate.value = "$service_name_template$.perfdata.graphitewriter_graphite_work_queue_item_rate.value"
+
+[icinga-graphite-queue.urlparams]
+title = "Graphite Queue"
+areaMode = "none"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icinga-graphite-queue.functions]
+graphitewriter_graphite_work_queue_items.value = "alias(color($metric$, '#1a7dd7'), 'Items')"
+graphitewriter_graphite_work_queue_item_rate.value = "alias(color($metric$, '#0b3c68'), 'Items/s')"
diff --git a/templates/icmp-hosts.ini b/templates/icmp-hosts.ini
new file mode 100644
index 0000000..e6122a3
--- /dev/null
+++ b/templates/icmp-hosts.ini
@@ -0,0 +1,38 @@
+[icmp-rt.graph]
+check_command = "icmp-host"
+
+[icmp-rt.metrics_filters]
+rtmin.value = "$host_name_template$.perfdata.rtmin.value"
+rta.value = "$host_name_template$.perfdata.rta.value"
+rtmax.value = "$host_name_template$.perfdata.rtmax.value"
+
+[icmp-rt.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icmp-rt.functions]
+rtmin.value = "alias(color(scale($metric$, 1000), '#44bb77'), 'Min. round trip time (ms)')"
+rta.value = "alias(color(scale($metric$, 1000), '#ffaa44'), 'Avg. round trip time (ms)')"
+rtmax.value = "alias(color(scale($metric$, 1000), '#ff5566'), 'Max. round trip time (ms)')"
+
+
+[icmp-pl.graph]
+check_command = "icmp-host"
+
+[icmp-pl.metrics_filters]
+pl.value = "$host_name_template$.perfdata.pl.value"
+
+[icmp-pl.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icmp-pl.functions]
+pl.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
+
+
diff --git a/templates/icmp.ini b/templates/icmp.ini
new file mode 100644
index 0000000..cb60655
--- /dev/null
+++ b/templates/icmp.ini
@@ -0,0 +1,36 @@
+[icmp-rt.graph]
+check_command = "icmp"
+
+[icmp-rt.metrics_filters]
+rtmin.value = "$service_name_template$.perfdata.rtmin.value"
+rta.value = "$service_name_template$.perfdata.rta.value"
+rtmax.value = "$service_name_template$.perfdata.rtmax.value"
+
+[icmp-rt.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icmp-rt.functions]
+rtmin.value = "alias(color(scale($metric$, 1000), '#44bb77'), 'Min. round trip time (ms)')"
+rta.value = "alias(color(scale($metric$, 1000), '#ffaa44'), 'Avg. round trip time (ms)')"
+rtmax.value = "alias(color(scale($metric$, 1000), '#ff5566'), 'Max. round trip time (ms)')"
+
+
+[icmp-pl.graph]
+check_command = "icmp"
+
+[icmp-pl.metrics_filters]
+pl.value = "$service_name_template$.perfdata.pl.value"
+
+[icmp-pl.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[icmp-pl.functions]
+pl.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
diff --git a/templates/interfacetable.ini b/templates/interfacetable.ini
new file mode 100644
index 0000000..359d201
--- /dev/null
+++ b/templates/interfacetable.ini
@@ -0,0 +1,52 @@
+[interfacetable-traffic.graph]
+check_command = "interfacetable"
+
+[interfacetable-traffic.metrics_filters]
+bpsin = "$service_name_template$.perfdata.If_$interface$.check_interface_table_port.BpsIn.value"
+bpsout = "$service_name_template$.perfdata.If_$interface$.check_interface_table_port.BpsOut.value"
+
+[interfacetable-traffic.urlparams]
+min = "0"
+title = "Interface $interface$"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interfacetable-traffic.functions]
+bpsin = "alias(color($metric$, '#1a7dd7'), 'Traffic in (B/s)')"
+bpsout = "alias(color($metric$, '#0b3c68'), 'Traffic out (B/s)')"
+
+
+[interfacetable-discard.graph]
+check_command = "interfacetable"
+
+[interfacetable-discard.metrics_filters]
+ppsindiscard = "$service_name_template$.perfdata.If_$interface$.check_interface_table_port.PpsInDiscard.value"
+ppsoutdiscard = "$service_name_template$.perfdata.If_$interface$.check_interface_table_port.PpsOutDiscard.value"
+
+[interfacetable-discard.urlparams]
+min = "0"
+title = "Interface $interface$"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interfacetable-discard.functions]
+ppsindiscard = "alias(color($metric$, '#edb017'), 'Discard in (B/s)')"
+ppsoutdiscard = "alias(color($metric$, '#ad7d05'), 'Discard out (B/s)')"
+
+
+[interfacetable-error.graph]
+check_command = "interfacetable"
+
+[interfacetable-error.metrics_filters]
+ppsinerr = "$service_name_template$.perfdata.If_$interface$.check_interface_table_port.PpsInErr.value"
+ppsouterr = "$service_name_template$.perfdata.If_$interface$.check_interface_table_port.PpsOutErr.value"
+
+[interfacetable-error.urlparams]
+min = "0"
+title = "Interface $interface$"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interfacetable-error.functions]
+ppsinerr = "alias(color($metric$, '#ff5566'), 'Error in (B/s)')"
+ppsouterr = "alias(color($metric$, '#a80000'), 'Error out (B/s)')"
diff --git a/templates/load.ini b/templates/load.ini
new file mode 100644
index 0000000..1a4de4d
--- /dev/null
+++ b/templates/load.ini
@@ -0,0 +1,35 @@
+[load.graph]
+check_command = "load"
+
+[load.metrics_filters]
+load15.value = "$service_name_template$.perfdata.load15.value"
+load5.value = "$service_name_template$.perfdata.load5.value"
+load1.value = "$service_name_template$.perfdata.load1.value"
+
+[load.urlparams]
+areaAlpha = "0.5"
+min = "0"
+yUnitSystem = "none"
+lineWidth = "2"
+
+[load.functions]
+load15.value = "alias(color($metric$, '#ff5566'), 'Load 15')"
+load5.value = "alias(color($metric$, '#ffaa44'), 'Load 5')"
+load1.value = "alias(color($metric$, '#44bb77'), 'Load 1')"
+
+
+[load-windows.graph]
+check_command = "load-windows"
+
+[load-windows.metrics_filters]
+value = "$service_name_template$.perfdata.load.value"
+
+[load-windows.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[load-windows.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Load (%)')"
diff --git a/templates/mailq.ini b/templates/mailq.ini
new file mode 100644
index 0000000..76b8a94
--- /dev/null
+++ b/templates/mailq.ini
@@ -0,0 +1,15 @@
+[mailq.graph]
+check_command = "mailq"
+
+[mailq.metrics_filters]
+unsent = "$service_name_template$.perfdata.unsent.value"
+
+[mailq.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mailq.functions]
+unsent = "alias(color($metric$, '#1a7dd7'), 'Unsent mails')"
diff --git a/templates/memory.ini b/templates/memory.ini
new file mode 100644
index 0000000..daba4b4
--- /dev/null
+++ b/templates/memory.ini
@@ -0,0 +1,17 @@
+[memory.graph]
+check_command = "memory-windows"
+
+[memory.metrics_filters]
+value = "$service_name_template$.perfdata.memory.value"
+max = "$service_name_template$.perfdata.memory.max"
+
+[memory.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "binary"
+
+[memory.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
diff --git a/templates/memory_windows_powershell_framework.ini b/templates/memory_windows_powershell_framework.ini
new file mode 100644
index 0000000..53f89f4
--- /dev/null
+++ b/templates/memory_windows_powershell_framework.ini
@@ -0,0 +1,42 @@
+[memory.graph]
+check_command = "Invoke-IcingaCheckMemory"
+
+[memory.metrics_filters]
+value = "$service_name_template$.perfdata.used_bytes.value"
+max = "$service_name_template$.perfdata.used_bytes.max"
+crit = "$service_name_template$.perfdata.used_bytes.crit"
+warn = "$service_name_template$.perfdata.used_bytes.warn"
+
+[memory.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Memory used"
+yUnitSystem = "binary"
+
+[memory.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
+
+[memory-percent.graph]
+check_command = "Invoke-IcingaCheckMemory"
+
+[memory-percent.metrics_filters]
+value = "$service_name_template$.perfdata.memory_percent_used.value"
+crit = "$service_name_template$.perfdata.memory_percent_used.crit"
+warn = "$service_name_template$.perfdata.memory_percent_used.warn"
+
+[memory-percent.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Memory % used"
+
+[memory-percent.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (%)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (%)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (%)')"
diff --git a/templates/multi.ini b/templates/multi.ini
new file mode 100644
index 0000000..2f35658
--- /dev/null
+++ b/templates/multi.ini
@@ -0,0 +1,54 @@
+[multi2-host.metrics_filters]
+value = "$host_name_template$.perfdata.$multi1$.$multi2$.value"
+
+[multi2-host.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+title = "$multi1$::$multi2$"
+yUnitSystem = "none"
+
+[multi2-host.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'value')"
+
+
+[multi2-service.metrics_filters]
+value = "$service_name_template$.perfdata.$multi1$.$multi2$.value"
+
+[multi2-service.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+title = "$multi1$::$multi2$"
+yUnitSystem = "none"
+
+[multi2-service.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'value')"
+
+
+[multi3-host.metrics_filters]
+value = "$host_name_template$.perfdata.$multi1$.$multi2$.$multi3$.value"
+
+[multi3-host.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+title = "$multi1$::$multi2$::$multi3$"
+yUnitSystem = "none"
+
+[multi3-host.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'value')"
+
+
+[multi3-service.metrics_filters]
+value = "$service_name_template$.perfdata.$multi1$.$multi2$.$multi3$.value"
+
+[multi3-service.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+title = "$multi1$::$multi2$::$multi3$"
+yUnitSystem = "none"
+
+[multi3-service.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'value')"
diff --git a/templates/mysql.ini b/templates/mysql.ini
new file mode 100644
index 0000000..f39d15a
--- /dev/null
+++ b/templates/mysql.ini
@@ -0,0 +1,270 @@
+[mysql-Connections.graph]
+check_command = "mysql"
+
+[mysql-Connections.metrics_filters]
+value = "$service_name_template$.perfdata.Connections.value"
+
+[mysql-Connections.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Connections.functions]
+value = "alias(color(nonNegativeDerivative($metric$), '#1a7dd7'), 'Connections')"
+
+
+[mysql-Open_files.graph]
+check_command = "mysql"
+
+[mysql-Open_files.metrics_filters]
+value = "$service_name_template$.perfdata.Open_files.value"
+
+[mysql-Open_files.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Open_files.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Open files')"
+
+
+[mysql-Open_tables.graph]
+check_command = "mysql"
+
+[mysql-Open_tables.metrics_filters]
+value = "$service_name_template$.perfdata.Open_tables.value"
+
+[mysql-Open_tables.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Open_tables.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Open tables')"
+
+
+[mysql-Qcache_free_memory.graph]
+check_command = "mysql"
+
+[mysql-Qcache_free_memory.metrics_filters]
+value = "$service_name_template$.perfdata.Qcache_free_memory.value"
+
+[mysql-Qcache_free_memory.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "binary"
+
+[mysql-Qcache_free_memory.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Query cache free memory (Bytes)')"
+
+
+[mysql-Qcache_hits.graph]
+check_command = "mysql"
+
+[mysql-Qcache_hits.metrics_filters]
+value = "$service_name_template$.perfdata.Qcache_hits.value"
+
+[mysql-Qcache_hits.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Qcache_hits.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Query cache hits')"
+
+
+[mysql-Qcache_inserts.graph]
+check_command = "mysql"
+
+[mysql-Qcache_inserts.metrics_filters]
+value = "$service_name_template$.perfdata.Qcache_inserts.value"
+
+[mysql-Qcache_inserts.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Qcache_inserts.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Query cache inserts')"
+
+
+[mysql-Qcache_lowmem_prunes.graph]
+check_command = "mysql"
+
+[mysql-Qcache_lowmem_prunes.metrics_filters]
+value = "$service_name_template$.perfdata.Qcache_lowmem_prunes.value"
+
+[mysql-Qcache_lowmem_prunes.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Qcache_lowmem_prunes.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Query cache lowmem prunes')"
+
+
+[mysql-Qcache_not_cached.graph]
+check_command = "mysql"
+
+[mysql-Qcache_not_cached.metrics_filters]
+value = "$service_name_template$.perfdata.Qcache_not_cached.value"
+
+[mysql-Qcache_not_cached.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Qcache_not_cached.functions]
+value = "alias(color(nonNegativeDerivative($metric$), '#1a7dd7'), 'Not cached queries')"
+
+
+[mysql-Qcache_queries_in_cache.graph]
+check_command = "mysql"
+
+[mysql-Qcache_queries_in_cache.metrics_filters]
+value = "$service_name_template$.perfdata.Qcache_queries_in_cache.value"
+
+[mysql-Qcache_queries_in_cache.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Qcache_queries_in_cache.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Queries in cache')"
+
+
+[mysql-Queries.graph]
+check_command = "mysql"
+
+[mysql-Queries.metrics_filters]
+value = "$service_name_template$.perfdata.Queries.value"
+
+[mysql-Queries.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Queries.functions]
+value = "alias(color(nonNegativeDerivative($metric$), '#1a7dd7'), 'Queries')"
+
+
+[mysql-Questions.graph]
+check_command = "mysql"
+
+[mysql-Questions.metrics_filters]
+value = "$service_name_template$.perfdata.Questions.value"
+
+[mysql-Questions.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Questions.functions]
+value = "alias(color(nonNegativeDerivative($metric$), '#1a7dd7'), 'Questions')"
+
+
+[mysql-Table_locks_waited.graph]
+check_command = "mysql"
+
+[mysql-Table_locks_waited.metrics_filters]
+value = "$service_name_template$.perfdata.Table_locks_waited.value"
+
+[mysql-Table_locks_waited.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Table_locks_waited.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Table locks waited for')"
+
+
+[mysql-Threads_connected.graph]
+check_command = "mysql"
+
+[mysql-Threads_connected.metrics_filters]
+value = "$service_name_template$.perfdata.Threads_connected.value"
+
+[mysql-Threads_connected.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Threads_connected.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Threads connected')"
+
+
+[mysql-Threads_running.graph]
+check_command = "mysql"
+
+[mysql-Threads_running.metrics_filters]
+value = "$service_name_template$.perfdata.Threads_running.value"
+
+[mysql-Threads_running.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql-Threads_running.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Threads running')"
+
+
+[mysql-Uptime.graph]
+check_command = "mysql"
+
+[mysql-Uptime.metrics_filters]
+value = "$service_name_template$.perfdata.Uptime.value"
+
+[mysql-Uptime.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[mysql-Uptime.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Uptime (s)')"
+
+
+[mysql-seconds_behind_master.graph]
+check_command = "mysql"
+
+[mysql-seconds_behind_master.metrics_filters]
+value = "$service_name_template$.perfdata.seconds_behind_master.value"
+
+[mysql-seconds_behind_master.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[mysql-seconds_behind_master.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Seconds behind master')"
diff --git a/templates/mysql_health.ini b/templates/mysql_health.ini
new file mode 100644
index 0000000..711338c
--- /dev/null
+++ b/templates/mysql_health.ini
@@ -0,0 +1,456 @@
+[mysql_health-bufferpool_hitrate.graph]
+check_command = "mysql_health"
+
+[mysql_health-bufferpool_hitrate.metrics_filters]
+value = "$service_name_template$.perfdata.bufferpool_hitrate.value"
+now.value = "$service_name_template$.perfdata.bufferpool_hitrate_now.value"
+
+[mysql_health-bufferpool_hitrate.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-bufferpool_hitrate.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'InnoDB buffer pool hitrate (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'InnoDB buffer pool hitrate (now, %)')"
+
+
+[mysql_health-bufferpool_wait_free.graph]
+check_command = "mysql_health"
+
+[mysql_health-bufferpool_wait_free.metrics_filters]
+value = "$service_name_template$.perfdata.bufferpool_free_waits_rate.value"
+
+[mysql_health-bufferpool_wait_free.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-bufferpool_wait_free.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'InnoDB buffer pool waits/s')"
+
+
+[mysql_health-clients_aborted.graph]
+check_command = "mysql_health"
+
+[mysql_health-clients_aborted.metrics_filters]
+value = "$service_name_template$.perfdata.clients_aborted_per_sec.value"
+
+[mysql_health-clients_aborted.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-clients_aborted.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Clients aborted/s')"
+
+
+[mysql_health-cluster_ndbd_running.graph]
+check_command = "mysql_health"
+
+[mysql_health-cluster_ndbd_running.metrics_filters]
+ndbd_nodes.value = "$service_name_template$.perfdata.ndbd_nodes.value"
+ndb_mgmd_nodes.value = "$service_name_template$.perfdata.ndb_mgmd_nodes.value"
+
+[mysql_health-cluster_ndbd_running.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-cluster_ndbd_running.functions]
+ndbd_nodes.value = "alias(color($metric$, '#1a7dd7'), 'Ndbd nodes')"
+ndb_mgmd_nodes.value = "alias(color($metric$, '#ff5566'), 'Ndb_mgmd nodes')"
+
+
+[mysql_health-mysqld_nodes.graph]
+check_command = "mysql_health"
+
+[mysql_health-mysqld_nodes.metrics_filters]
+value = "$service_name_template$.perfdata.mysqld_nodes.value"
+
+[mysql_health-mysqld_nodes.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-mysqld_nodes.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'MySQLd nodes')"
+
+
+[mysql_health-connection_time.graph]
+check_command = "mysql_health"
+
+[mysql_health-connection_time.metrics_filters]
+value = "$service_name_template$.perfdata.connection_time.value"
+
+[mysql_health-connection_time.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-connection_time.functions]
+value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Connection time (ms)')"
+
+
+[mysql_health-connects_aborted.graph]
+check_command = "mysql_health"
+
+[mysql_health-connects_aborted.metrics_filters]
+value = "$service_name_template$.perfdata.connects_aborted_per_sec.value"
+
+[mysql_health-connects_aborted.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-connects_aborted.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Connects aborted/s')"
+
+
+[mysql_health-index_usage.graph]
+check_command = "mysql_health"
+
+[mysql_health-index_usage.metrics_filters]
+value = "$service_name_template$.perfdata.index_usage.value"
+now.value = "$service_name_template$.perfdata.index_usage_now.value"
+
+[mysql_health-index_usage.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-index_usage.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Index usage (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'Index usage (now, %)')"
+
+
+[mysql_health-keycache_hitrate.graph]
+check_command = "mysql_health"
+
+[mysql_health-keycache_hitrate.metrics_filters]
+value = "$service_name_template$.perfdata.keycache_hitrate.value"
+now.value = "$service_name_template$.perfdata.keycache_hitrate_now.value"
+
+[mysql_health-keycache_hitrate.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-keycache_hitrate.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'MyISAM keycache hitrate (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'MyISAM keycache hitrate (now, %)')"
+
+
+[mysql_health-log_waits.graph]
+check_command = "mysql_health"
+
+[mysql_health-log_waits.metrics_filters]
+value = "$service_name_template$.perfdata.innodb_log_waits_rate.value"
+
+[mysql_health-log_waits.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-log_waits.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'InnoDB log waits/s')"
+
+
+[mysql_health-long_running_procs.graph]
+check_command = "mysql_health"
+
+[mysql_health-long_running_procs.metrics_filters]
+value = "$service_name_template$.perfdata.long_running_procs.value"
+
+[mysql_health-long_running_procs.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-long_running_procs.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Long running processes')"
+
+
+[mysql_health-open_files.graph]
+check_command = "mysql_health"
+
+[mysql_health-open_files.metrics_filters]
+value = "$service_name_template$.perfdata.open_files.value"
+
+[mysql_health-open_files.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-open_files.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Open files')"
+
+
+[mysql_health-qcache_hitrate.graph]
+check_command = "mysql_health"
+
+[mysql_health-qcache_hitrate.metrics_filters]
+value = "$service_name_template$.perfdata.qcache_hitrate.value"
+now.value = "$service_name_template$.perfdata.qcache_hitrate_now.value"
+
+[mysql_health-qcache_hitrate.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-qcache_hitrate.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Query cache hitrate (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'Query cache hitrate (now, %)')"
+
+
+[mysql_health-selects_per_sec.graph]
+check_command = "mysql_health"
+
+[mysql_health-selects_per_sec.metrics_filters]
+value = "$service_name_template$.perfdata.selects_per_sec.value"
+
+[mysql_health-selects_per_sec.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-selects_per_sec.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Selects/s')"
+
+
+[mysql_health-qcache_lowmem_prunes.graph]
+check_command = "mysql_health"
+
+[mysql_health-qcache_lowmem_prunes.metrics_filters]
+value = "$service_name_template$.perfdata.qcache_lowmem_prunes_rate.value"
+
+[mysql_health-qcache_lowmem_prunes.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-qcache_lowmem_prunes.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Query cache lowmem prunes/s')"
+
+
+[mysql_health-slave_lag.graph]
+check_command = "mysql_health"
+
+[mysql_health-slave_lag.metrics_filters]
+value = "$service_name_template$.perfdata.slave_lag.value"
+
+[mysql_health-slave_lag.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[mysql_health-slave_lag.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Seconds slave is behind master')"
+
+
+[mysql_health-slow_queries.graph]
+check_command = "mysql_health"
+
+[mysql_health-slow_queries.metrics_filters]
+value = "$service_name_template$.perfdata.slow_queries_rate.value"
+
+[mysql_health-slow_queries.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-slow_queries.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Slow queries/s')"
+
+
+[mysql_health-table_lock_contention.graph]
+check_command = "mysql_health"
+
+[mysql_health-table_lock_contention.metrics_filters]
+value = "$service_name_template$.perfdata.tablelock_contention.value"
+now.value = "$service_name_template$.perfdata.tablelock_contention_now.value"
+
+[mysql_health-table_lock_contention.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-table_lock_contention.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Table lock contention (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'Table lock contention (now, %)')"
+
+
+[mysql_health-tablecache_hitrate.graph]
+check_command = "mysql_health"
+
+[mysql_health-tablecache_hitrate.metrics_filters]
+value = "$service_name_template$.perfdata.tablecache_hitrate.value"
+
+[mysql_health-tablecache_hitrate.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-tablecache_hitrate.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Table cache hitrate (%)')"
+
+
+[mysql_health-tablecache_fillrate.graph]
+check_command = "mysql_health"
+
+[mysql_health-tablecache_fillrate.metrics_filters]
+value = "$service_name_template$.perfdata.tablecache_fillrate.value"
+
+[mysql_health-tablecache_fillrate.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-tablecache_fillrate.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Table cache fillrate (%)')"
+
+
+[mysql_health-threadcache_hitrate.graph]
+check_command = "mysql_health"
+
+[mysql_health-threadcache_hitrate.metrics_filters]
+value = "$service_name_template$.perfdata.thread_cache_hitrate.value"
+now.value = "$service_name_template$.perfdata.thread_cache_hitrate_now.value"
+
+[mysql_health-threadcache_hitrate.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-threadcache_hitrate.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Thread cache hitrate (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'Thread cache hitrate (now, %)')"
+
+
+[mysql_health-threads_cached.graph]
+check_command = "mysql_health"
+
+[mysql_health-threads_cached.metrics_filters]
+value = "$service_name_template$.perfdata.threads_cached.value"
+
+[mysql_health-threads_cached.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-threads_cached.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Threads cached')"
+
+
+[mysql_health-threads_connected.graph]
+check_command = "mysql_health"
+
+[mysql_health-threads_connected.metrics_filters]
+value = "$service_name_template$.perfdata.threads_connected.value"
+
+[mysql_health-threads_connected.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-threads_connected.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Threads connected')"
+
+
+[mysql_health-threads_created.graph]
+check_command = "mysql_health"
+
+[mysql_health-threads_created.metrics_filters]
+value = "$service_name_template$.perfdata.threads_created_per_sec.value"
+
+[mysql_health-threads_created.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-threads_created.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Threads created/s')"
+
+
+[mysql_health-threads_running.graph]
+check_command = "mysql_health"
+
+[mysql_health-threads_running.metrics_filters]
+value = "$service_name_template$.perfdata.threads_running.value"
+
+[mysql_health-threads_running.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-threads_running.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Threads running')"
+
+
+[mysql_health-tmp_disk_tables.graph]
+check_command = "mysql_health"
+
+[mysql_health-tmp_disk_tables.metrics_filters]
+value = "$service_name_template$.perfdata.pct_tmp_table_on_disk.value"
+now.value = "$service_name_template$.perfdata.pct_tmp_table_on_disk_now.value"
+
+[mysql_health-tmp_disk_tables.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[mysql_health-tmp_disk_tables.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Temp tables created on disk (%)')"
+now.value = "alias(color($metric$, '#ff5566'), 'Temp tables created on disk (now, %)')"
diff --git a/templates/netapp_cdot_aggregates.ini b/templates/netapp_cdot_aggregates.ini
new file mode 100644
index 0000000..5e26e6f
--- /dev/null
+++ b/templates/netapp_cdot_aggregates.ini
@@ -0,0 +1,22 @@
+[disk.graph]
+check_command = "netapp_cdot_aggr_status"
+
+[disk.metrics_filters]
+value = "$service_name_template$.perfdata.$disk$.value"
+max = "$service_name_template$.perfdata.$disk$.max"
+crit = "$service_name_template$.perfdata.$disk$.crit"
+warn = "$service_name_template$.perfdata.$disk$.warn"
+[disk.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $disk$"
+yUnitSystem = "binary"
+
+[disk.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
+
diff --git a/templates/netapp_cdot_volumes.ini b/templates/netapp_cdot_volumes.ini
new file mode 100644
index 0000000..92cc686
--- /dev/null
+++ b/templates/netapp_cdot_volumes.ini
@@ -0,0 +1,44 @@
+[vol-space.graph]
+check_command = "netapp_cdot_volume_status"
+
+[vol-space.metrics_filters]
+value = "$service_name_template$.perfdata.$vol$.check_cdot_volume_usage.space_used.value"
+max = "$service_name_template$.perfdata.$vol$.check_cdot_volume_usage.space_used.max"
+crit = "$service_name_template$.perfdata.$vol$.check_cdot_volume_usage.space_used.crit"
+warn = "$service_name_template$.perfdata.$vol$.check_cdot_volume_usage.space_used.warn"
+[vol-space.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $vol$ Space used"
+yUnitSystem = "binary"
+
+[vol-space.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
+
+[disk-inode.graph]
+check_command = "netapp_cdot_volume_status"
+
+[disk-inode.metrics_filters]
+value = "$service_name_template$.perfdata.$disk$.check_cdot_volume_usage.inode_used.value"
+max = "$service_name_template$.perfdata.$disk$.check_cdot_volume_usage.inode_used.max"
+crit = "$service_name_template$.perfdata.$disk$.check_cdot_volume_usage.inode_used.crit"
+warn = "$service_name_template$.perfdata.$disk$.check_cdot_volume_usage.inode_used.warn"
+
+[disk-inode.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $disk$ Inodes used"
+
+[disk-inode.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used Inodes')"
+max = "alias(color($metric$, '#cfd7e6'), 'Max Inodes')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit Inodes')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn Inodes')"
+
diff --git a/templates/network.ini b/templates/network.ini
new file mode 100644
index 0000000..e7801e6
--- /dev/null
+++ b/templates/network.ini
@@ -0,0 +1,35 @@
+[network-windows-total.graph]
+check_command = "network-windows"
+
+[network-windows-total.metrics_filters]
+value = "$service_name_template$.perfdata.network.value"
+
+[network-windows-total.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+title = "Total"
+yUnitSystem = "binary"
+
+[network-windows-total.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'I/O (bytes/s)')"
+
+
+[network-windows-nic.graph]
+check_command = "network-windows"
+
+[network-windows-nic.metrics_filters]
+in.value = "$service_name_template$.perfdata.$nic$_in.value"
+out.value = "$service_name_template$.perfdata.$nic$_out.value"
+
+[network-windows-nic.urlparams]
+areaAlpha = "0.5"
+lineWidth = "2"
+min = "0"
+title = "$nic$"
+yUnitSystem = "binary"
+
+[network-windows-nic.functions]
+in.value = "alias(color($metric$, '#1a7dd7'), 'In (bytes/s)')"
+out.value = "alias(color($metric$, '#ff5566'), 'Out (bytes/s)')"
diff --git a/templates/ntp.ini b/templates/ntp.ini
new file mode 100644
index 0000000..be91992
--- /dev/null
+++ b/templates/ntp.ini
@@ -0,0 +1,65 @@
+[ntp-offset.graph]
+check_command = "ntp_time, ntp_peer"
+
+[ntp-offset.metrics_filters]
+value = "$service_name_template$.perfdata.offset.value"
+
+[ntp-offset.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+yUnitSystem = "none"
+
+[ntp-offset.functions]
+value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Offset (ms)')"
+
+
+[ntp_peer-jitter.graph]
+check_command = "ntp_peer"
+
+[ntp_peer-jitter.metrics_filters]
+value = "$service_name_template$.perfdata.jitter.value"
+
+[ntp_peer-jitter.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[ntp_peer-jitter.functions]
+value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Jitter (ms)')"
+
+
+[ntp_peer-stratum.graph]
+check_command = "ntp_peer"
+
+[ntp_peer-stratum.metrics_filters]
+value = "$service_name_template$.perfdata.stratum.value"
+
+[ntp_peer-stratum.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[ntp_peer-stratum.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Stratum')"
+
+
+[ntp_peer-truechimers.graph]
+check_command = "ntp_peer"
+
+[ntp_peer-truechimers.metrics_filters]
+value = "$service_name_template$.perfdata.truechimers.value"
+
+[ntp_peer-truechimers.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[ntp_peer-truechimers.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Truechimers')"
diff --git a/templates/nwc-interface-usage.ini b/templates/nwc-interface-usage.ini
new file mode 100644
index 0000000..8d9cf7a
--- /dev/null
+++ b/templates/nwc-interface-usage.ini
@@ -0,0 +1,58 @@
+[interface-traffic.graph]
+check_command = "interface-usage"
+
+[interface-traffic.metrics_filters]
+bpsin = "$service_name_template$.perfdata.$interface$_traffic_in.value"
+bpsmaxin = "$service_name_template$.perfdata.$interface$_traffic_in.max"
+bpscritin = "$service_name_template$.perfdata.$interface$_traffic_in.crit"
+bpswarnin = "$service_name_template$.perfdata.$interface$_traffic_in.warn"
+
+bpsout = "$service_name_template$.perfdata.$interface$_traffic_out.value"
+bpsmaxout = "$service_name_template$.perfdata.$interface$_traffic_out.max"
+bpscritout = "$service_name_template$.perfdata.$interface$_traffic_out.crit"
+bpswarnout = "$service_name_template$.perfdata.$interface$_traffic_out.warn"
+
+[interface-traffic.urlparams]
+min = "0"
+title = "$interface$ traffic"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interface-traffic.functions]
+bpsin = "alias(color($metric$, '#1a7dd7'), 'Traffic in (B/s)')"
+bpsmaxin = "alias(color($metric$, '#7d7f81'), 'Traffic in max (B/s)')"
+bpswarnin = "alias(color($metric$, '#ff8d00'), 'Traffic in warn (B/s)')"
+bpscritin = "alias(color($metric$, '#ff0000'), 'Traffic in crit (B/s)')"
+
+bpsout = "alias(color($metric$, '#0b3c68'), 'Traffic out (B/s)')"
+bpsmaxout = "alias(color($metric$, '#45008d'), 'Traffic out max (B/s)')"
+bpswarnout = "alias(color($metric$, '#ff8d00'), 'Traffic out warn (B/s)')"
+bpscritout = "alias(color($metric$, '#ff0000'), 'Traffic out crit (B/s)')"
+
+
+[interface-usage.graph]
+check_command = "interface-usage"
+
+[interface-usage.metrics_filters]
+usagein = "$service_name_template$.perfdata.$interface$_usage_in.value"
+usagecritin = "$service_name_template$.perfdata.$interface$_usage_in.crit"
+usagewarnin = "$service_name_template$.perfdata.$interface$_usage_in.warn"
+
+usageout = "$service_name_template$.perfdata.$interface$_usage_out.value"
+usagecritout = "$service_name_template$.perfdata.$interface$_usage_out.crit"
+usagewarnout = "$service_name_template$.perfdata.$interface$_usage_out.warn"
+
+[interface-usage.urlparams]
+min = "0"
+title = "$interface$ usage"
+lineWidth = "2"
+yUnitSystem = "none"
+
+[interface-usage.functions]
+usagein = "alias(color($metric$, '#1a7dd7'), 'Usage in (%)')"
+usagewarnin = "alias(color($metric$, '#ff8d00'), 'Usage in warn (%)')"
+usagecritin = "alias(color($metric$, '#ff0000'), 'Usage in crit (%)')"
+
+usageout = "alias(color($metric$, '#0b3c68'), 'Usage out (%)')"
+usagewarnout = "alias(color($metric$, '#ff8d00'), 'Usage out warn (%)')"
+usagecritout = "alias(color($metric$, '#ff0000'), 'Usage out crit (%)')"
diff --git a/templates/nwc-load.ini b/templates/nwc-load.ini
new file mode 100644
index 0000000..83d080f
--- /dev/null
+++ b/templates/nwc-load.ini
@@ -0,0 +1,19 @@
+[load.graph]
+check_command = "nwc-load"
+
+[load.metrics_filters]
+value = "$service_name_template$.nwc_health.perfdata.$load$.value"
+crit = "$service_name_template$.nwc_health.perfdata.$load$.crit"
+warn = "$service_name_template$.nwc_health.perfdata.$load$.warn"
+
+[load.urlparams]
+areaAlpha = "0.5"
+lineWidth = "2"
+min = "0"
+title = "$load$ %"
+yUnitSystem = "none"
+
+[load.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'CPU usage (%)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (%)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (%)')"
diff --git a/templates/ping.ini b/templates/ping.ini
new file mode 100644
index 0000000..7d281d0
--- /dev/null
+++ b/templates/ping.ini
@@ -0,0 +1,32 @@
+[ping-rta.graph]
+check_command = "ping, ping4, ping6, ping-windows"
+
+[ping-rta.metrics_filters]
+rta.value = "$service_name_template$.perfdata.rta.value"
+
+[ping-rta.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[ping-rta.functions]
+rta.value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Round trip time (ms)')"
+
+
+[ping-pl.graph]
+check_command = "ping, ping4, ping6"
+
+[ping-pl.metrics_filters]
+pl.value = "$service_name_template$.perfdata.pl.value"
+
+[ping-pl.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[ping-pl.functions]
+pl.value = "alias(color($metric$, '#1a7dd7'), 'Packet loss (%)')"
diff --git a/templates/procs.ini b/templates/procs.ini
new file mode 100644
index 0000000..2fcc374
--- /dev/null
+++ b/templates/procs.ini
@@ -0,0 +1,15 @@
+[procs.graph]
+check_command = "procs, procs-windows"
+
+[procs.metrics_filters]
+value = "$service_name_template$.perfdata.procs.value"
+
+[procs.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[procs.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Process count')"
diff --git a/templates/response-size-hosts.ini b/templates/response-size-hosts.ini
new file mode 100644
index 0000000..e8c80cc
--- /dev/null
+++ b/templates/response-size-hosts.ini
@@ -0,0 +1,16 @@
+[response-size.graph]
+check_command = "http-host"
+
+[response-size.metrics_filters]
+value = "$host_name_template$.perfdata.size.value"
+
+[response-size.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "binary"
+
+[response-size.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Response size (bytes)')"
+
diff --git a/templates/response-size.ini b/templates/response-size.ini
new file mode 100644
index 0000000..7171d07
--- /dev/null
+++ b/templates/response-size.ini
@@ -0,0 +1,15 @@
+[response-size.graph]
+check_command = "http"
+
+[response-size.metrics_filters]
+value = "$service_name_template$.perfdata.size.value"
+
+[response-size.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "binary"
+
+[response-size.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Response size (bytes)')"
diff --git a/templates/response-time-hosts.ini b/templates/response-time-hosts.ini
new file mode 100644
index 0000000..7986733
--- /dev/null
+++ b/templates/response-time-hosts.ini
@@ -0,0 +1,16 @@
+[response-time.graph]
+check_command = "dig-host, dns-host, ftp-host, http-host, imap-host, ldap-host, pgsql-host, smtp-host, ssh-host, tcp-host, udp-host"
+
+[response-time.metrics_filters]
+value = "$host_name_template$.perfdata.time.value"
+
+[response-time.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[response-time.functions]
+value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Response time (ms)')"
+
diff --git a/templates/response-time.ini b/templates/response-time.ini
new file mode 100644
index 0000000..222492f
--- /dev/null
+++ b/templates/response-time.ini
@@ -0,0 +1,15 @@
+[response-time.graph]
+check_command = "dig, dns, ftp, http, imap, ldap, pgsql, smtp, ssh, tcp, udp"
+
+[response-time.metrics_filters]
+value = "$service_name_template$.perfdata.time.value"
+
+[response-time.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[response-time.functions]
+value = "alias(color(scale($metric$, 1000), '#1a7dd7'), 'Response time (ms)')"
diff --git a/templates/snmp-int.ini b/templates/snmp-int.ini
new file mode 100644
index 0000000..32a87ad
--- /dev/null
+++ b/templates/snmp-int.ini
@@ -0,0 +1,54 @@
+[interfacetable-traffic.graph]
+check_command = "snmp-interface"
+
+[interfacetable-traffic.metrics_filters]
+bpsin = "$service_name_template$.perfdata.$interface$_in_bps.value"
+bpsout = "$service_name_template$.perfdata.$interface$_out_bps.value"
+
+[interfacetable-traffic.urlparams]
+min = "0"
+title = "Interface $interface$ Traffic"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interfacetable-traffic.functions]
+bpsin = "alias(color($metric$, '#1a7dd7'), 'Traffic in (B/s)')"
+bpsout = "alias(color($metric$, '#0b3c68'), 'Traffic out (B/s)')"
+
+
+[interfacetable-discard.graph]
+check_command = "snmp-interface"
+
+[interfacetable-discard.metrics_filters]
+ppsindiscard = "$service_name_template$.perfdata.$interface$_in_discard.value"
+ppsoutdiscard = "$service_name_template$.perfdata.$interface$_out_discard.value"
+
+[interfacetable-discard.urlparams]
+min = "0"
+title = "Interface $interface$ Discards"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interfacetable-discard.functions]
+ppsindiscard = "alias(color($metric$, '#edb017'), 'Discard in (B/s)')"
+ppsoutdiscard = "alias(color($metric$, '#ad7d05'), 'Discard out (B/s)')"
+
+
+[interfacetable-error.graph]
+check_command = "snmp-interface"
+
+[interfacetable-error.metrics_filters]
+ppsinerr = "$service_name_template$.perfdata.$interface$_in_error.value"
+ppsouterr = "$service_name_template$.perfdata.$interface$_out_error.value"
+
+[interfacetable-error.urlparams]
+min = "0"
+title = "Interface $interface$ Errors"
+lineWidth = "2"
+yUnitSystem = "si"
+
+[interfacetable-error.functions]
+ppsinerr = "alias(color($metric$, '#ff5566'), 'Error in (B/s)')"
+ppsouterr = "alias(color($metric$, '#a80000'), 'Error out (B/s)')"
+
+
diff --git a/templates/snmp-load-netsl.ini b/templates/snmp-load-netsl.ini
new file mode 100644
index 0000000..85e6df6
--- /dev/null
+++ b/templates/snmp-load-netsl.ini
@@ -0,0 +1,19 @@
+[load-snmp.graph]
+check_command = "snmp-load"
+
+[load-snmp.metrics_filters]
+load15.value = "$service_name_template$.perfdata.load_15_min.value"
+load5.value = "$service_name_template$.perfdata.load_5_min.value"
+load1.value = "$service_name_template$.perfdata.load_1_min.value"
+
+[load-snmp.urlparams]
+areaAlpha = "0.5"
+min = "0"
+yUnitSystem = "none"
+lineWidth = "2"
+
+[load-snmp.functions]
+load15.value = "alias(color($metric$, '#ff5566'), 'Load 15')"
+load5.value = "alias(color($metric$, '#ffaa44'), 'Load 5')"
+load1.value = "alias(color($metric$, '#44bb77'), 'Load 1')"
+
diff --git a/templates/snmp-load-stand.ini b/templates/snmp-load-stand.ini
new file mode 100644
index 0000000..f7b11e8
--- /dev/null
+++ b/templates/snmp-load-stand.ini
@@ -0,0 +1,20 @@
+[load-windows.graph]
+check_command = "load-windows"
+
+[load-windows.metrics_filters]
+load.value = "$service_name_template$.perfdata.cpu_prct_used.value"
+crit.value = "$service_name_template$.perfdata.cpu_prct_used.crit"
+warn.value = "$service_name_template$.perfdata.cpu_prct_used.warn"
+
+[load-windows.urlparams]
+areaAlpha = "0.5"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[load-windows.functions]
+load.value = "alias(color($metric$, '#1a7dd7'), 'CPU usage(%)')"
+crit.value = "alias(color($metric$, '#ff0000'), 'Crit (%)')"
+warn.value = "alias(color($metric$, '#ff8d00'), 'Warn (%)')"
+
+
diff --git a/templates/snmp-memory.ini b/templates/snmp-memory.ini
new file mode 100644
index 0000000..8b154da
--- /dev/null
+++ b/templates/snmp-memory.ini
@@ -0,0 +1,23 @@
+[memory.graph]
+check_command = "snmp-memory"
+
+[memory.metrics_filters]
+value = "$service_name_template$.perfdata.$mem$.value"
+max = "$service_name_template$.perfdata.$mem$.max"
+crit = "$service_name_template$.perfdata.$mem$.crit"
+warn = "$service_name_template$.perfdata.$mem$.warn"
+
+[memory.urlparams]
+areaAlpha = "0.5"
+lineWidth = "2"
+min = "0"
+title = "Memory $mem$"
+yUnitSystem = "binary"
+
+[memory.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
+
+
diff --git a/templates/snmp-storage.ini b/templates/snmp-storage.ini
new file mode 100644
index 0000000..4b4505f
--- /dev/null
+++ b/templates/snmp-storage.ini
@@ -0,0 +1,21 @@
+[disk.graph]
+check_command = "snmp-storage"
+
+[disk.metrics_filters]
+value = "$service_name_template$.perfdata.$disk$.value"
+max = "$service_name_template$.perfdata.$disk$.max"
+crit = "$service_name_template$.perfdata.$disk$.crit"
+warn = "$service_name_template$.perfdata.$disk$.warn"
+[disk.urlparams]
+areaAlpha = "0.5"
+areaMode = "first"
+lineWidth = "2"
+min = "0"
+title = "Disk $disk$"
+yUnitSystem = "binary"
+
+[disk.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
+crit = "alias(color($metric$, '#ff0000'), 'Crit (bytes)')"
+warn = "alias(color($metric$, '#ff8d00'), 'Warn (bytes)')"
diff --git a/templates/swap.ini b/templates/swap.ini
new file mode 100644
index 0000000..535bc69
--- /dev/null
+++ b/templates/swap.ini
@@ -0,0 +1,17 @@
+[swap.graph]
+check_command = "swap, swap-windows"
+
+[swap.metrics_filters]
+value = "$service_name_template$.perfdata.swap.value"
+max = "$service_name_template$.perfdata.swap.max"
+
+[swap.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "binary"
+
+[swap.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Used (bytes)')"
+max = "alias(color($metric$, '#cfd7e6'), 'Size (bytes)')"
diff --git a/templates/update.ini b/templates/update.ini
new file mode 100644
index 0000000..3b87ea7
--- /dev/null
+++ b/templates/update.ini
@@ -0,0 +1,34 @@
+[apt.graph]
+check_command = "apt"
+
+[apt.metrics_filters]
+critical_updates.value = "$service_name_template$.perfdata.critical_updates.value"
+available_upgrades.value = "$service_name_template$.perfdata.available_upgrades.value"
+
+[apt.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[apt.functions]
+critical_updates.value = "alias(color($metric$, '#ff5566'), 'Critical Updates')"
+available_upgrades.value = "alias(color($metric$, '#ffaa44'), 'Available Upgrades')"
+
+
+[update-windows.graph]
+check_command = "update-windows"
+
+[update-windows.metrics_filters]
+value = "$service_name_template$.perfdata.update.value"
+
+[update-windows.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[update-windows.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Available Updates')"
diff --git a/templates/uptime.ini b/templates/uptime.ini
new file mode 100644
index 0000000..588ab2c
--- /dev/null
+++ b/templates/uptime.ini
@@ -0,0 +1,32 @@
+[uptime.graph]
+check_command = "mysql_health, uptime-windows"
+
+[uptime.metrics_filters]
+value = "$service_name_template$.perfdata.uptime.value"
+
+[uptime.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[uptime.functions]
+value = "alias(color($metric$, '#1a7dd7'), 'Uptime (s)')"
+
+
+[uptime-snmp.graph]
+check_command = "snmp-uptime"
+
+[uptime-snmp.metrics_filters]
+value = "$service_name_template$.perfdata.uptime.value"
+
+[uptime-snmp.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "sec"
+
+[uptime-snmp.functions]
+value = "alias(color(scale($metric$, 86400), '#1a7dd7'), 'Uptime (s)')"
diff --git a/templates/users.ini b/templates/users.ini
new file mode 100644
index 0000000..64172b8
--- /dev/null
+++ b/templates/users.ini
@@ -0,0 +1,15 @@
+[users.graph]
+check_command = "users, users-windows"
+
+[users.metrics_filters]
+users = "$service_name_template$.perfdata.users.value"
+
+[users.urlparams]
+areaAlpha = "0.5"
+areaMode = "all"
+lineWidth = "2"
+min = "0"
+yUnitSystem = "none"
+
+[users.functions]
+users = "alias(color($metric$, '#1a7dd7'), 'Logged in Users')"