pax_global_header00006660000000000000000000000064124077504010014512gustar00rootroot0000000000000052 comment=1b37395a664605edbf3ccf321041282f12555388 Nagstamon/000077500000000000000000000000001240775040100130055ustar00rootroot00000000000000Nagstamon/COPYRIGHT000066400000000000000000000000661240775040100143020ustar00rootroot00000000000000Copyright (C) 2009 Henri Wahl Nagstamon/ChangeLog000066400000000000000000000270071240775040100145650ustar00rootroot00000000000000nagstamon (1.0.1) stable; urgency=low * New upstream - added option to disable system keyring storage to prevent crashes - reverted default sorting order to "Descending" - fixed too narrow fullscreen display - fixed vanishing Nagstamon submenu in Ubuntu Appindicator -- Henri Wahl Mon, 22 Sep 2014 9:00:00 +0200 nagstamon (1.0) stable; urgency=low * New upstream - added custom event notification with custom commands - added highlighting of new events - added storage of passwords in OS keyring - added optional tooltip for full status information - added support for applying custom actions to specific monitor only - added copy buttons for servers and actions dialogs - added stopping notification if event already vanished - added support for Op5Monitor 6.3 instead of Ninja - added experimental Zabbix support - added automatic refreshing after acknowledging - added permanent hamburger menu - unified layout of dialogs - various Check_MK improvements - fixed old regression not-staying-on-top-bug - fixed Check_MK-Recheck-DOS-bug - fixed pop window size calculation on multiple screens - fixed following popup window on multiple screens - fixed hiding dialogs in MacOSX - fixed ugly statusbar font in MacOSX - fixed use of changed colors - fixed non-ascending default sort order - fixed Opsview downtime dialog - fixed sometimes not working context menu - fixed some GUI glitches - fixed password saving bug - fixed Centreon language inconsistencies - fixed regression Umlaut bug -- Henri Wahl Mon, 28 Jul 2014 09:30:00 +0200 nagstamon (1.0rc2) unstable; urgency=low * New upstream - added automatic refreshing after acknowledging - added permanent hamburger menu - unified layout of dialogs - fixed some GUI glitches - fixed password saving bug - fixed Centreon language inconsistencies - fixed regression Umlaut bug -- Henri Wahl Tue, 08 Jul 2014 11:00:00 +0200 nagstamon (1.0rc1) unstable; urgency=low * New upstream - added custom event notification with custom commands - added highlighting of new events - added storage of passwords in OS keyring - added optional tooltip for full status information - added support for applying custom actions to specific monitor only - added copy buttons for servers and actions dialogs - added stopping notification if event already vanished - added support for Op5Monitor 6.3 instead of Ninja - added experimental Zabbix support - fixed old regression not-staying-on-top-bug - fixed Check_MK-Recheck-DOS-bug - fixed pop window size calculation on multiple screens - fixed following popup window on multiple screens - fixed hiding dialogs in MacOSX - fixed ugly statusbar font in MacOSX - fixed use of changed colors - fixed non-ascending default sort order - fixed Opsview downtime dialog - fixed sometimes not working context menu - various Check_MK improvements -- Henri Wahl Tue, 24 Jun 2014 11:00:00 +0200 nagstamon (0.9.11) stable; urgency=low * New upstream - added Ubuntu AppIndicator support - added libnotify desktop notification support - added Centreon criticality support - fixed broken authentication dialog - fixed wrong OK state for Nagios and Icinga - fixed Correct-Statusbar-Position-O-Matic - fixed some Thruk issues - fixed popup resizing artefact - fixed some server edit dialog bugs - fixed missing auth field in Icinga when credentials are wrong - fixed quoting URLs for browser actions -- Henri Wahl Wed, 11 Sep 2013 09:00:00 +0200 nagstamon (0.9.11rc1) unstable; urgency=low * New upstream - added Ubuntu AppIndicator support - added libnotify desktop notification support - added Centreon criticality support - fixed broken authentication dialog - fixed wrong OK state for Nagios and Icinga - fixed Correct-Statusbar-Position-O-Matic - fixed some Thruk issues - fixed popup resizing artefact - fixed some server edit dialog bugs - fixed missing auth field in Icinga when credentials are wrong -- Henri Wahl Mon, 29 Jul 2013 10:35:00 +0200 nagstamon (0.9.10) stable; urgency=low * New upstream - added fullscreen option - added Thruk support - added Check_MK cookie-based auth - added new Centreon autologin option - added configurable default sort order - added filter for hosts in hard/soft state for Nagios, Icinga, Opsview and Centreon - added $STATUS-INFO$ variable for custom actions - added audio alarms also in fullscreen mode - improved update interval set in seconds instead minutes - improved Icinga JSON support - improved Centreon 2.4 xml/broker support - improved Nagios 3.4 pagination support - improved nicer GTK theme Murrine on MacOSX - fixed security bug - fixed some memory leaks - fixed superfluous passive icon for Check_MK - fixed blocking of shutdown/reboot on MacOSX - fixed saving converted pre 0.9.9 config immediately - fixed statusbar position when offscreen - fixed some GUI issues - fixed update detection -- Henri Wahl Wed, 11 Jul 2013 11:07:13 +0200 nagstamon (0.9.10rc2) unstable; urgency=low * New upstream - audio alarms also in fullscreen mode - adjust x0 y0 position of statusbar when offscreen - save converted pre 0.9.9 config immediately -- Henri Wahl Tue, 09 Jul 2013 14:25:00 +0200 nagstamon (0.9.10rc1) unstable; urgency=low * New upstream - added fullscreen option - added Thruk support - added Check_MK cookie-based auth - added new Centreon autologin option - added configurable default sort order - added filter for hosts in hard/soft state for Nagios, Icinga, Opsview and Centreon - added $STATUS-INFO$ variable for custom actions - update interval set in seconds instead minutes - improved Icinga JSON support - improved Centreon 2.4 xml/broker support - improved Nagios 3.4 pagination support - uses nicer GTK theme Murrine on MacOSX - fixed some memory leaks - fixed superfluous passive icon for Check_MK - fixed blocking of shutdown/reboot on MacOSX - fixed some GUI issues - fixed update detection -- Henri Wahl Wed, 03 Jul 2013 10:25:00 +0200 nagstamon (0.9.9.1-1) stable; urgency=low * New upstream - added custom actions in context menu - added reauthentication in case of authenticaton problems - changed configuration file to configuration directory (default: ~/.nagstamon) - added filter for flapping hosts and services - added history button for monitors - added shortcut to filter settings in popup window - improved keyboard usage in acknowledge/downtime/submit dialogs - fixed bug in Icinga acknowledgement - fixed bug in Check_MK Multisite sorting - fixed some Check_MK Multisite UTF trouble - fixed some GUI artefacts when resizing popup window -- Henri Wahl Fri, 13 Apr 2012 11:25:00 +0200 nagstamon (0.9.8.1-1) stable; urgency=low * New upstream - added customizable acknowledge/downtime/submit-result defaults - added regexp filter for status information column - added option to connect to hosts via its monitor hostname without HTTP overhead - added ability to keep status detail popup open despite hovering away - added option to change offset between popup window and systray icon to avoid partly hidden popup - fixed some popup artefacts - fixed various bugs with acknowledgement flags (persistent/sticky/notification), now they are actually working - fixed some issues when running on MacOS X -- Henri Wahl Wed, 10 Oct 2011 14:49:00 +0200 nagstamon (0.9.7.1-1) stable; urgency=low * New upstream - hot fix for broken Centreon support - sf.net bug 3309166 -- Henri Wahl Fri, 30 May 2011 12:01:00 +0200 nagstamon (0.9.7-1) stable; urgency=low * New upstream - on some servers now context menu allows submitting check results for hosts and services - added filter for services on acknowledged hosts - added icons for "passiveonly" and "flapping" hosts and services - fix for uneditable text entry fields in settings dialog - sf.net bug 3300873 - fix for not working filter "services on hosts in maintenance" - sf.net bug 3299790 - fix for soft state detection in Centreon - sf.net bug 3303861 - fix for not filtered services which should have been filtered - sf.net bug 3308008 -- Henri Wahl Fri, 27 May 2011 16:01:00 +0200 nagstamon (0.9.6.1-1) stable; urgency=low * fix for sf.net bug 3298321 - displaying error when all is OK -- Henri Wahl Fri, 06 May 2011 16:01:00 +0200 nagstamon (0.9.6-1) stable; urgency=low * New upstreeam release - improved, full Ninja support - rewritten filtering mechanism allows new features - displaying icons in status overview popup indicating states "acknowledged" and "scheduled downtime" - added option to play notification sounds more than once - small UI improvements - uses BeautifulSoup instead of lxml - uses GTK UI Builder instead of glade - as always: bugfixes -- Henri Wahl Fri, 06 May 2011 16:01:00 +0200 nagstamon (0.9.5-1) stable; urgency=low * New upstream release - added op5 Ninja support - added Check_MK Multisite support - improved Icinga support (compatibility with Icinga 1.3) - improved Centreon support (compatible with Centreon 2.1) - added sortable columns in status overview - added customizable colors - better debugging and error messages - password must not be stored in config file - major memory leak closed, various bugs fixed -- Henri Wahl Tue, 05 Apr 2011 13:23:00 +0200 nagstamon (0.9.4-1) stable; urgency=low * New upstream release (Closes: #582977) * removed debian/manpages * renamed debian/nagstamon.install to debian/install * debian/patches - removed settings_glade - removed setup_patch -- Carl Chenet Sat, 19 Jun 2010 12:46:42 +0200 nagstamon (0.9.3-2) stable; urgency=low * debian/patches/default_search - disable the default search for newer versions (Closes: #585928) * debian/patches/series - added default_search -- Carl Chenet Tue, 15 Jun 2010 00:41:22 +0200 nagstamon (0.9.3-1) stable; urgency=low * New upstream release * Switching to 3.0 source format * debian/patches - added settings_glade patch to close #2998035 upstream bug - added setup_patch to remove an absolute link for the upstream manpage * debian/control - added quilt in Build-Depends - added the support of Opsview servers in the long description * debian/nagstamon.manpages - switched to the file provided by the upstream * debian/nagstamon.desktop - commenting the OnlyShowIn directive -- Carl Chenet Sun, 23 May 2010 12:47:11 +0200 nagstamon (0.9.2-2) stable; urgency=low * debian/control - Added a mandatory runtime missing dependency python-pkg-resources. - Fixed a typo in the long message. -- Carl Chenet Wed, 24 Mar 2010 23:18:21 +0100 nagstamon (0.9.2-1) stable; urgency=low * Initial release. (Closes: #534842) -- Carl Chenet Mon, 22 Feb 2010 14:16:44 +0100 Nagstamon/LICENSE000066400000000000000000001323301240775040100140140ustar00rootroot00000000000000Nagstamon is licensed under the following GPL2: 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. Copyright (C) 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. , 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. Nagstamon uses BeautifulSoup under the following license: Copyright (c) 2004-2010, Leonard Richardson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the the Beautiful Soup Consortium and All Night Kosher Bakery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. Nagstamon's experimental Zabbix support is based on zabbix_api.py, which is licensed under LGPL 2.1: GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 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. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, 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 library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete 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 distribute a copy of this License along with the Library. 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 Library or any portion of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, 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 Library, 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 Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you 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. If distribution of 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 satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be 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. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library 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. 9. 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 Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library 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 with this License. 11. 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 Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library 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 Library. 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. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library 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. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library 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 Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, 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 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. 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 LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. 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. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; 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. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it!Nagstamon/Nagstamon/000077500000000000000000000000001240775040100147345ustar00rootroot00000000000000Nagstamon/Nagstamon/Actions.py000066400000000000000000001347171240775040100167230ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import threading import gobject import time import datetime import urllib import webbrowser import subprocess import re import sys import traceback import gtk # if running on windows import winsound import platform if platform.system() == "Windows": import winsound # Garbage collection import gc # import for MultipartPostHandler.py which is needed for Opsview downtime form import urllib2 import mimetools, mimetypes import os, stat from Nagstamon import Objects from Nagstamon.Objects import Result #from Nagstamon import GUI import GUI # import md5 for centreon url autologin encoding try: #from python 2.5 md5 is in hashlib from hashlib import md5 except: # older pythons use md5 lib from md5 import md5 # flag which indicates if already rechecking all RecheckingAll = False def StartRefreshLoop(servers=None, output=None, conf=None): """ the everlasting refresh cycle - starts refresh cycle for every server as thread """ for server in servers.values(): if str(conf.servers[server.get_name()].enabled) == "True": server.thread = RefreshLoopOneServer(server=server, output=output, conf=conf) server.thread.start() class RefreshLoopOneServer(threading.Thread): """ one thread for one server per loop """ # kind of a stop please flag, if set to True run() should run no more stopped = False # Check flag, if set and thread recognizes do a refresh, set to True at the beginning doRefresh = True def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] # include threading mechanism threading.Thread.__init__(self, name=self.server.get_name()) self.setDaemon(1) def Stop(self): # simply sets the stopped flag to True to let the above while stop this thread when checking next self.stopped = True def Refresh(self): # simply sets the stopped flag to True to let the above while stop this thread when checking next self.doRefresh = True def run(self): """ loop until end of eternity or until server is stopped """ # do stuff like getting server version and setting some URLs self.server.init_config() while self.stopped == False: # check if we have to leave update interval sleep if self.server.count > int(self.conf.update_interval_seconds): self.doRefresh = True # self.doRefresh could also been changed by RefreshAllServers() if self.doRefresh == True: # reset server count self.server.count = 0 # check if server is already checked if self.server.isChecking == False: # set server status for status field in popwin self.server.status = "Refreshing (last updated %s)" % time.ctime() gobject.idle_add(self.output.popwin.UpdateStatus, self.server) # get current status server_status = self.server.GetStatus(output=self.output) # GTK/Pango does not like tag brackets < and >, so clean them out from description server_status.error = server_status.error.replace("<", "").replace(">", "").replace("\n", " ") # debug if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.get_name(), debug="server return values: " + str(server_status.result) + " " + str(server_status.error)) if server_status.error != "": # set server status for status field in popwin self.server.status = "ERROR" # give server status description for future usage self.server.status_description = str(server_status.error) gobject.idle_add(self.output.popwin.UpdateStatus, self.server) # tell gobject to care about GUI stuff - refresh display status # use a flag to prevent all threads at once to write to statusbar label in case # of lost network connectivity - this leads to a mysterious pango crash if self.output.statusbar.isShowingError == False: gobject.idle_add(self.output.RefreshDisplayStatus) if str(self.conf.fullscreen) == "True": gobject.idle_add(self.output.popwin.RefreshFullscreen) # wait a moment time.sleep(5) # change statusbar to the following error message # show error message in statusbar # shorter error message - see https://sourceforge.net/tracker/?func=detail&aid=3017044&group_id=236865&atid=1101373 gobject.idle_add(self.output.statusbar.ShowErrorMessage, {"True":"ERROR", "False":"ERR"}[str(self.conf.long_display)]) # wait some seconds time.sleep(5) # set statusbar error message status back self.output.statusbar.isShowingError = False # wait a moment time.sleep(10) else: # set server status for status field in popwin self.server.status = "Connected (last updated %s)" % time.ctime() # tell gobject to care about GUI stuff - refresh display status gobject.idle_add(self.output.RefreshDisplayStatus) if str(self.conf.fullscreen) == "True": gobject.idle_add(self.output.popwin.RefreshFullscreen) # wait for the doRefresh flag to be True, if it is, do a refresh if self.doRefresh == True: if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.get_name(), debug="Refreshing output - server is already checking: " + str(self.server.isChecking)) # reset refresh flag self.doRefresh = False # call Hook() for extra action self.server.Hook() else: # sleep and count time.sleep(1) self.server.count += 1 # call Hook() for extra action self.server.Hook() # refresh fullscreen window - maybe somehow raw approach if str(self.conf.fullscreen) == "True": gobject.idle_add(self.output.popwin.RefreshFullscreen) def RefreshAllServers(servers=None, output=None, conf=None): """ one refreshing action, starts threads, one per polled server """ # first delete all freshness flags output.UnfreshEventHistory() for server in servers.values(): # check if server is already checked if server.isChecking == False and str(conf.servers[server.get_name()].enabled) == "True": #debug if str(conf.debug_mode) == "True": server.Debug(server=server.get_name(), debug="Checking server...") server.thread.Refresh() # set server status for status field in popwin server.status = "Refreshing" gobject.idle_add(output.popwin.UpdateStatus, server) class DebugLoop(threading.Thread): """ run and empty debug_queue into debug log file """ # stop flag stopped = False def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] # check if DebugLoop is already looping - if it does do not run another one for t in threading.enumerate(): if t.getName() == "DebugLoop": # loop gets stopped as soon as it starts - maybe waste self.stopped = True # initiate Loop try: threading.Thread.__init__(self, name="DebugLoop") self.setDaemon(1) except Exception, err: print err # open debug file if needed if str(self.conf.debug_to_file) == "True" and self.stopped == False: try: self.debug_file = open(self.conf.debug_file, "w") except Exception, err: # if path to file does not exist tell user self.output.Dialog(message=err) def run(self): # as long as debugging is wanted do it while self.stopped == False and str(self.conf.debug_mode) == "True": # .get() waits until there is something to get - needs timeout in case no debug messages fly in debug_string = "" try: debug_string = self.debug_queue.get(True, 1) print debug_string if str(self.conf.debug_to_file) == "True" and self.__dict__.has_key("debug_file") and debug_string != "": self.debug_file.write(debug_string + "\n") except: pass # if no debugging is needed anymore stop it if str(self.conf.debug_mode) == "False": self.stopped = True def Stop(self): # simply sets the stopped flag to True to let the above while stop this thread when checking next self.stopped = True class Recheck(threading.Thread): """ recheck a clicked service/host """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self, name=self.server.get_name() + "-Recheck") self.setDaemon(1) def run(self): try: self.server.set_recheck(self) except: self.server.Error(sys.exc_info()) class RecheckAll(threading.Thread): """ recheck all services/hosts """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self, name="RecheckAll") self.setDaemon(1) def run(self): # get RecheckingAll flag to decide if rechecking all is possible (only if not already running) global RecheckingAll if RecheckingAll == False: RecheckingAll = True # put all rechecking threads into one dictionary rechecks_dict = dict() try: # debug if str(self.conf.debug_mode) == "True": # workaround, take Debug method from first server reachable self.servers.values()[0].Debug(debug="Recheck all: Rechecking all services on all hosts on all servers...") for server in self.servers.values(): # only test enabled servers and only if not already if str(self.conf.servers[server.get_name()].enabled) == "True": # set server status for status field in popwin server.status = "Rechecking all started" gobject.idle_add(self.output.popwin.UpdateStatus, server) # special treatment for Check_MK Multisite because there is only one URL call necessary if server.type != "Check_MK Multisite": for host in server.hosts.values(): # construct an unique key which refers to rechecking thread in dictionary rechecks_dict[server.get_name() + ": " + host.get_name()] = Recheck(server=server, host=host.get_name(), service="") rechecks_dict[server.get_name() + ": " + host.get_name()].start() # debug if str(self.conf.debug_mode) == "True": server.Debug(server=server.get_name(), host=host.get_name(), debug="Rechecking...") for service in host.services.values(): # dito if service.is_passive_only() == True: continue rechecks_dict[server.get_name() + ": " + host.get_name() + ": " + service.get_name()] = Recheck(server=server, host=host.get_name(), service=service.get_name()) rechecks_dict[server.get_name() + ": " + host.get_name() + ": " + service.get_name()].start() # debug if str(self.conf.debug_mode) == "True": server.Debug(server=server.get_name(), host=host.get_name(), service=service.get_name(), debug="Rechecking...") else: # Check_MK Multisite does it its own way server.recheck_all() # wait until all rechecks have been done while len(rechecks_dict) > 0: # debug if str(self.conf.debug_mode) == "True": # once again taking .Debug() from first server self.servers.values()[0].Debug(server=server.get_name(), debug="Recheck all: # of checks which still need to be done: " + str(len(rechecks_dict))) for i in rechecks_dict.copy(): # if a thread is stopped pop it out of the dictionary if rechecks_dict[i].isAlive() == False: rechecks_dict.pop(i) # wait a second time.sleep(1) # debug if str(self.conf.debug_mode) == "True": # once again taking .Debug() from first server self.servers.values()[0].Debug(server=server.get_name(), debug="Recheck all: All servers, hosts and services are rechecked.") # reset global flag RecheckingAll = False # after all and after a short delay to let the monitor apply the recheck requests refresh all to make changes visible soon time.sleep(5) RefreshAllServers(servers=self.servers, output=self.output, conf=self.conf) # do some cleanup del rechecks_dict except: RecheckingAll = False else: # debug if str(self.conf.debug_mode) == "True": # once again taking .Debug() from first server self.servers.values()[0].Debug(debug="Recheck all: Already rechecking all services on all hosts on all servers.") class Acknowledge(threading.Thread): """ exceute remote cgi command with parameters from acknowledge dialog """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): self.server.set_acknowledge(self) class Downtime(threading.Thread): """ exceute remote cgi command with parameters from acknowledge dialog """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): self.server.set_downtime(self) def Downtime_get_start_end(server, host): # get start and end time from Nagios as HTML - the objectified HTML does not contain the form elements :-( # this used to happen in GUI.action_downtime_dialog_show but for a more strict separation it better stays here return server.get_start_end(host) class SubmitCheckResult(threading.Thread): """ exceute remote cgi command with parameters from submit check result dialog """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): self.server.set_submit_check_result(self) class CheckForNewVersion(threading.Thread): """ Check for new version of nagstamon using connections of configured servers """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): """ try all servers respectively their net connections, one of them should be able to connect to nagstamon.sourceforge.net """ # debug if str(self.output.conf.debug_mode) == "True": # once again taking .Debug() from first server self.servers.values()[0].Debug(debug="Checking for new version...") for s in self.servers.values(): # if connecton of a server is not yet used do it now if s.CheckingForNewVersion == False: # set the flag to lock that connection s.CheckingForNewVersion = True # use IFW server to speed up request and secure via https result = s.FetchURL("https://nagstamon.ifw-dresden.de/files-nagstamon/latest_version_" +\ self.output.version, giveback="raw", no_auth=True) # remove newline version, error = result.result.split("\n")[0], result.error # debug if str(self.output.conf.debug_mode) == "True": # once again taking .Debug() from first server self.servers.values()[0].Debug(debug="Latest version: " + str(version)) # if we got a result notify user if error == "": if version == self.output.version: version_status = "latest" else: version_status = "out_of_date" # if we got a result reset all servers checkfornewversion flags, # notify the user and break out of the for loop for s in self.servers.values(): s.CheckingForNewVersion = False # do not tell user that the version is latest when starting up nagstamon if not (self.mode == "startup" and version_status == "latest"): # gobject.idle_add is necessary to start gtk stuff from thread gobject.idle_add(self.output.CheckForNewVersionDialog, version_status, version) break # reset the servers CheckingForNewVersion flag to allow a later check s.CheckingForNewVersion = False class PlaySound(threading.Thread): """ play notification sound in a threadified way to omit hanging gui """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): if self.sound == "WARNING": if str(self.conf.notification_default_sound) == "True": self.Play(self.Resources + "/warning.wav") else: self.Play(self.conf.notification_custom_sound_warning) elif self.sound == "CRITICAL": if str(self.conf.notification_default_sound) == "True": self.Play(self.Resources + "/critical.wav") else: self.Play(self.conf.notification_custom_sound_critical) elif self.sound == "DOWN": if str(self.conf.notification_default_sound) == "True": self.Play(self.Resources + "/hostdown.wav") else: self.Play(self.conf.notification_custom_sound_down) elif self.sound =="FILE": self.Play(self.file) def Play(self, file): """ depending on platform choose method to play sound """ # debug if str(self.conf.debug_mode) == "True": # once again taking .Debug() from first server self.servers.values()[0].Debug(debug="Playing sound: " + str(file)) if not platform.system() == "Windows": subprocess.Popen("play -q %s" % str(file), shell=True) else: winsound.PlaySound(file, winsound.SND_FILENAME) class Notification(threading.Thread): """ Flash statusbar in a threadified way to omit hanging gui """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): # counter for repeated sound soundcount = 0 # in case of notifying in statusbar do some flashing and honking while self.output.Notifying == True: # as long as flashing flag is set statusbar flashes until someone takes care if self.output.statusbar.Flashing == True: if self.output.statusbar.isShowingError == False: # check again because in the mean time this flag could have been changed by NotificationOff() gobject.idle_add(self.output.statusbar.Flash) # Ubuntu AppIndicator simulates flashing by brute force if str(self.conf.appindicator) == "True": if self.output.appindicator.Flashing == True: gobject.idle_add(self.output.appindicator.Flash) # if wanted play notification sound, if it should be repeated every minute (2*interval/0.5=interval) do so. if str(self.conf.notification_sound) == "True": if soundcount == 0: sound = PlaySound(sound=self.sound, Resources=self.Resources, conf=self.conf, servers=self.servers) sound.start() soundcount += 1 elif str(self.conf.notification_sound_repeat) == "True" and\ soundcount >= 2*int(self.conf.update_interval_seconds) and\ len([k for k,v in self.output.events_history.items() if v == True]) != 0: soundcount = 0 else: soundcount += 1 time.sleep(0.5) # reset statusbar self.output.statusbar.Label.set_markup(self.output.statusbar.statusbar_labeltext) class MoveStatusbar(threading.Thread): """ Move statusbar in a threadified way to omit hanging gui and Windows-GTK 2.22 trouble """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): # avoid flickering popwin while moving statusbar around # gets re-enabled from popwin.setShowable() if self.output.GUILock.has_key("Popwin"): self.output.popwin.Close() self.output.popwin.showPopwin = False # lock GUI while moving statusbar so no auth dialogs could pop up self.output.AddGUILock(self.__class__.__name__) # in case of moving statusbar do some moves while self.output.statusbar.Moving == True: gobject.idle_add(self.output.statusbar.Move) time.sleep(0.01) self.output.DeleteGUILock(self.__class__.__name__) class Action(threading.Thread): """ Execute custom actions triggered by context menu of popwin parameters are action and hosts/service """ def __init__(self, **kwds): # add all keywords to object self.host = "" self.service = "" self.status_info = "" for k in kwds: self.__dict__[k] = kwds[k] threading.Thread.__init__(self) self.setDaemon(1) def run(self): # first replace placeholder variables in string with actual values """ Possible values for variables: $HOST$ - host as in monitor $SERVICE$ - service as in monitor $MONITOR$ - monitor address - not yet clear what exactly for $MONITOR-CGI$ - monitor CGI address - not yet clear what exactly for $ADDRESS$ - address of host, investigated by Server.GetHost() $STATUS-INFO$ - status information $USERNAME$ - username on monitor $PASSWORD$ - username's password on monitor - whatever for $COMMENT-ACK$ - default acknowledge comment $COMMENT-DOWN$ - default downtime comment $COMMENT-SUBMIT$ - default submit check result comment """ try: # if run as custom action use given action definition from conf, otherwise use for URLs if self.__dict__.has_key("action"): string = self.action.string action_type = self.action.type else: string = self.string action_type = self.type # used for POST request if self.__dict__.has_key("cgi_data"): cgi_data = self.cgi_data else: cgi_data = "" # mapping of variables and values mapping = { "$HOST$": self.host,\ "$SERVICE$": self.service,\ "$ADDRESS$": self.server.GetHost(self.host).result,\ "$MONITOR$": self.server.monitor_url,\ "$MONITOR-CGI$": self.server.monitor_cgi_url,\ "$STATUS-INFO$": self.status_info,\ "$USERNAME$": self.server.username,\ "$PASSWORD$": self.server.password,\ "$COMMENT-ACK$": self.conf.defaults_acknowledge_comment,\ "$COMMENT-DOWN$": self.conf.defaults_downtime_comment,\ "$COMMENT-SUBMIT$": self.conf.defaults_submit_check_result_comment, } # mapping mapping for i in mapping: string = string.replace(i, mapping[i]) # see what action to take if action_type == "browser": # debug if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.name, host=self.host, service=self.service, debug="ACTION: BROWSER " + string) webbrowser.open(string) elif action_type == "command": # debug if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.name, host=self.host, service=self.service, debug="ACTION: COMMAND " + string) subprocess.Popen(string, shell=True) elif action_type == "url": # Check_MK uses transids - if this occurs in URL its very likely that a Check_MK-URL is called if "$TRANSID$" in string: transid = self.server._get_transid(self.host, self.service) string = string.replace("$TRANSID$", transid).replace(" ", "+") else: # make string ready for URL string = self._URLify(string) # debug if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.name, host=self.host, service=self.service, debug="ACTION: URL in background " + string) self.server.FetchURL(string) # used for example by Op5Monitor.py elif action_type == "url-post": # make string ready for URL string = self._URLify(string) # debug if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.name, host=self.host, service=self.service, debug="ACTION: URL-POST in background " + string) self.server.FetchURL(string, cgi_data=cgi_data) # special treatment for Check_MK/Multisite Transaction IDs, called by Multisite._action() elif action_type == "url-check_mk-multisite": if "?_transid=-1&" in string: # Python format is of no use her, only web interface gives an transaction id # since werk #0766 http://mathias-kettner.de/check_mk_werks.php?werk_id=766 a real transid is needed transid = self.server._get_transid(self.host, self.service) # insert fresh transid string = string.replace("?_transid=-1&", "?_transid=%s&" % (transid)) string = string + "&actions=yes" if self.service != "": # if service exists add it and convert spaces to + string = string + "&service=%s" % (self.service.replace(" ", "+")) # debug if str(self.conf.debug_mode) == "True": self.server.Debug(server=self.server.name, host=self.host, service=self.service, debug="ACTION: URL-Check_MK in background " + string) self.server.FetchURL(string) except: import traceback traceback.print_exc(file=sys.stdout) def _URLify(self, string): """ return a string that fulfills requirements for URLs exclude several chars """ return urllib.quote(string, ":/=?&@+") class LonesomeGarbageCollector(threading.Thread): """ do repeatedly collect some garbage - before every server thread did but might make more sense done at one place and time """ def __init__(self): # garbage collection gc.enable() threading.Thread.__init__(self) self.setDaemon(1) def run(self): while True: gc.collect() # lets do a gc.collect() once every minute time.sleep(60) def TreeViewNagios(server, host, service): # if the clicked row does not contain a service it mus be a host, # so the nagios query is different server.open_tree_view(host, service) # contains dict with available server classes # key is type of server, value is server class # used for automatic config generation # and holding this information in one place REGISTERED_SERVERS = [] def register_server(server): """ Once new server class in created, should be registered with this function for being visible in config and accessible in application. """ if server.TYPE not in [x[0] for x in REGISTERED_SERVERS]: REGISTERED_SERVERS.append((server.TYPE, server)) def get_registered_servers(): """ Returns available server classes dict """ return dict(REGISTERED_SERVERS) def get_registered_server_type_list(): """ Returns available server type name list with order of registering """ return [x[0] for x in REGISTERED_SERVERS] def CreateServer(server=None, conf=None, debug_queue=None, resources=None): # create Server from config registered_servers = get_registered_servers() if server.type not in registered_servers: print 'Server type not supported: %s' % server.type return # give argument servername so CentreonServer could use it for initializing MD5 cache new_server = registered_servers[server.type](conf=conf, name=server.name) new_server.type = server.type new_server.monitor_url = server.monitor_url new_server.monitor_cgi_url = server.monitor_cgi_url # add resources, needed for auth dialog new_server.Resources = resources new_server.username = server.username new_server.password = server.password new_server.use_proxy = server.use_proxy new_server.use_proxy_from_os = server.use_proxy_from_os new_server.proxy_address = server.proxy_address new_server.proxy_username = server.proxy_username new_server.proxy_password = server.proxy_password # if password is not to be saved ask for it at startup if ( server.enabled == "True" and server.save_password == "False" and server.use_autologin == "False" ): new_server.refresh_authentication = True # access to thread-safe debug queue new_server.debug_queue = debug_queue # use server-owned attributes instead of redefining them with every request new_server.passman = urllib2.HTTPPasswordMgrWithDefaultRealm() new_server.passman.add_password(None, server.monitor_url, server.username, server.password) new_server.passman.add_password(None, server.monitor_cgi_url, server.username, server.password) new_server.basic_handler = urllib2.HTTPBasicAuthHandler(new_server.passman) new_server.digest_handler = urllib2.HTTPDigestAuthHandler(new_server.passman) new_server.proxy_auth_handler = urllib2.ProxyBasicAuthHandler(new_server.passman) if str(new_server.use_proxy) == "False": # use empty proxyhandler new_server.proxy_handler = urllib2.ProxyHandler({}) elif str(server.use_proxy_from_os) == "False": # if proxy from OS is not used there is to add a authenticated proxy handler new_server.passman.add_password(None, new_server.proxy_address, new_server.proxy_username, new_server.proxy_password) new_server.proxy_handler = urllib2.ProxyHandler({"http": new_server.proxy_address, "https": new_server.proxy_address}) new_server.proxy_auth_handler = urllib2.ProxyBasicAuthHandler(new_server.passman) # Special FX # Centreon new_server.use_autologin = server.use_autologin new_server.autologin_key = server.autologin_key # Icinga new_server.use_display_name_host = server.use_display_name_host new_server.use_display_name_service = server.use_display_name_service # create permanent urlopener for server to avoid memory leak with millions of openers new_server.urlopener = BuildURLOpener(new_server) # server's individual preparations for HTTP connections (for example cookie creation), version of monitor if str(server.enabled) == "True": new_server.init_HTTP() # debug if str(conf.debug_mode) == "True": new_server.Debug(server=server.name, debug="Created server.") return new_server def not_empty(x): '''tiny helper function for BeautifulSoup in GenericServer.py to filter text elements''' return bool(x.replace(' ', '').strip()) def BuildURLOpener(server): """ if there should be no proxy used use an empty proxy_handler - only necessary in Windows, where IE proxy settings are used automatically if available In UNIX $HTTP_PROXY will be used The MultipartPostHandler is needed for submitting multipart forms from Opsview """ # trying with changed digest/basic auth order as some digest auth servers do not # seem to work wi the previous way if str(server.use_proxy) == "False": server.proxy_handler = urllib2.ProxyHandler({}) urlopener = urllib2.build_opener(server.digest_handler,\ server.basic_handler,\ server.proxy_handler,\ urllib2.HTTPCookieProcessor(server.Cookie),\ MultipartPostHandler) elif str(server.use_proxy) == "True": if str(server.use_proxy_from_os) == "True": urlopener = urllib2.build_opener(server.digest_handler,\ server.basic_handler,\ urllib2.HTTPCookieProcessor(server.Cookie),\ MultipartPostHandler) else: # if proxy from OS is not used there is to add a authenticated proxy handler server.passman.add_password(None, server.proxy_address, server.proxy_username, server.proxy_password) server.proxy_handler = urllib2.ProxyHandler({"http": server.proxy_address, "https": server.proxy_address}) server.proxy_auth_handler = urllib2.ProxyBasicAuthHandler(server.passman) urlopener = urllib2.build_opener(server.proxy_handler,\ server.proxy_auth_handler,\ server.digest_handler,\ server.basic_handler,\ urllib2.HTTPCookieProcessor(server.Cookie),\ MultipartPostHandler) return urlopener def OpenNagstamonDownload(output=None): """ Opens Nagstamon Download page after being offered by update check """ # first close popwin output.popwin.Close() # start browser with URL webbrowser.open("https://nagstamon.ifw-dresden.de/download") def IsFoundByRE(string, pattern, reverse): """ helper for context menu actions in context menu - hosts and services might be filtered out also useful for services and hosts and status information """ pattern = re.compile(pattern) if len(pattern.findall(string)) > 0: if str(reverse) == "True": return False else: return True else: if str(reverse) == "True": return True else: return False def HostIsFilteredOutByRE(host, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if str(conf.re_host_enabled) == "True": return IsFoundByRE(host, conf.re_host_pattern, conf.re_host_reverse) # if RE are disabled return True because host is not filtered return False except: import traceback traceback.print_exc(file=sys.stdout) def ServiceIsFilteredOutByRE(service, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if str(conf.re_service_enabled) == "True": return IsFoundByRE(service, conf.re_service_pattern, conf.re_service_reverse) # if RE are disabled return True because host is not filtered return False except: import traceback traceback.print_exc(file=sys.stdout) def StatusInformationIsFilteredOutByRE(status_information, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if str(conf.re_status_information_enabled) == "True": return IsFoundByRE(status_information, conf.re_status_information_pattern, conf.re_status_information_reverse) # if RE are disabled return True because host is not filtered return False except: import traceback traceback.print_exc(file=sys.stdout) def CriticalityIsFilteredOutByRE(criticality, conf=None): """ helper for applying RE filters in Generic.GetStatus() """ try: if str(conf.re_criticality_enabled) == "True": return IsFoundByRE(criticality, conf.re_criticality_pattern, conf.re_criticality_reverse) # if RE are disabled return True because host is not filtered return False except: import traceback traceback.print_exc(file=sys.stdout) def HumanReadableDurationFromSeconds(seconds): """ convert seconds given by Opsview to the form Nagios gives them like 70d 3h 34m 34s """ timedelta = str(datetime.timedelta(seconds=int(seconds))) try: if timedelta.find("day") == -1: hms = timedelta.split(":") if len(hms) == 1: return "0d 0h 0m %ss" % (hms[0]) elif len(hms) == 2: return "0d 0h %sm %ss" % (hms[0], hms[1]) else: return "0d %sh %sm %ss" % (hms[0], hms[1], hms[2]) else: # waste is waste - does anyone need it? days, waste, hms = str(timedelta).split(" ") hms = hms.split(":") return "%sd %sh %sm %ss" % (days, hms[0], hms[1], hms[2]) except: # in case of any error return seconds we got return seconds def HumanReadableDurationFromTimestamp(timestamp): """ Thruk server supplies timestamp of latest state change which has to be subtracted from .now() """ try: td = datetime.datetime.now() - datetime.datetime.fromtimestamp(int(timestamp)) h = td.seconds / 3600 m = td.seconds % 3600 / 60 s = td.seconds % 60 return "%sd %sh %sm %ss" % (td.days, h, m ,s) except: import traceback traceback.print_exc(file=sys.stdout) def MachineSortableDate(raw): """ Monitors gratefully show duration even in weeks and months which confuse the sorting of popup window sorting - this functions wants to fix that """ # dictionary for duration date string components d = {"M":0, "w":0, "d":0, "h":0, "m":0, "s":0} # if for some reason the value is empty/none make it compatible: 0s if raw == None: raw = "0s" # strip and replace necessary for Nagios duration values, # split components of duration into dictionary for c in raw.strip().replace(" ", " ").split(" "): number, period = c[0:-1],c[-1] d[period] = int(number) del number, period # convert collected duration data components into seconds for being comparable return 16934400 * d["M"] + 604800 * d["w"] + 86400 * d["d"] + 3600 * d["h"] + 60 * d["m"] + d["s"] def MachineSortableDateMultisite(raw): """ Multisite dates/times are so different to the others so it has to be handled separately """ # dictionary for duration date string components d = {"M":0, "d":0, "h":0, "m":0, "s":0} # if for some reason the value is empty/none make it compatible: 0 sec if raw == None: raw = "0 sec" # check_mk has different formats - if duration takes too long it changes its scheme if "-" in raw and ":" in raw: datepart, timepart = raw.split(" ") # need to convert years into months for later comparison Y, M, D = datepart.split("-") d["M"] = int(Y) * 12 + int(M) d["d"] = int(D) # time does not need to be changed h, m, s = timepart.split(":") d["h"], d["m"], d["s"] = int(h), int(m), int(s) del datepart, timepart, Y, M, D, h, m, s else: # recalculate a timedelta of the given value if "sec" in raw: d["s"] = raw.split(" ")[0] delta = datetime.datetime.now() - datetime.timedelta(seconds=int(d["s"])) elif "min" in raw: d["m"] = raw.split(" ")[0] delta = datetime.datetime.now() - datetime.timedelta(minutes=int(d["m"])) elif "hrs" in raw: d["h"] = raw.split(" ")[0] delta = datetime.datetime.now() - datetime.timedelta(hours=int(d["h"])) elif "days" in raw: d["d"] = raw.split(" ")[0] delta = datetime.datetime.now() - datetime.timedelta(days=int(d["d"])) else: delta = datetime.datetime.now() Y, M, d["d"], d["h"], d["m"], d["s"] = delta.strftime("%Y %m %d %H %M %S").split(" ") # need to convert years into months for later comparison d["M"] = int(Y) * 12 + int(M) # int-ify d for i in d: d[i] = int(d[i]) # convert collected duration data components into seconds for being comparable return 16934400 * d["M"] + 86400 * d["d"] + 3600 * d["h"] + 60 * d["m"] + d["s"] def MD5ify(string): """ makes something md5y of a given username or password for Centreon web interface access """ return md5(string).hexdigest() def RunNotificationAction(action): """ run action for notification """ subprocess.Popen(action, shell=True) # # Borrowed from http://pipe.scs.fsu.edu/PostHandler/MultipartPostHandler.py # Released under LGPL # Thank you Will Holcomb! class Callable: def __init__(self, anycallable): self.__call__ = anycallable class MultipartPostHandler(urllib2.BaseHandler): handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first def http_request(self, request): data = request.get_data() if data is not None and type(data) != str: v_vars = [] try: for(key, value) in data.items(): v_vars.append((key, value)) except TypeError: systype, value, traceback = sys.exc_info() raise TypeError, "not a valid non-string sequence or mapping object", traceback boundary, data = self.multipart_encode(v_vars) contenttype = 'multipart/form-data; boundary=%s' % boundary if(request.has_header('Content-Type') and request.get_header('Content-Type').find('multipart/form-data') != 0): print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') request.add_unredirected_header('Content-Type', contenttype) request.add_data(data) return request def multipart_encode(vars, boundary = None, buffer = None): if boundary is None: boundary = mimetools.choose_boundary() if buffer is None: buffer = '' for(key, value) in vars: buffer += '--%s\r\n' % boundary buffer += 'Content-Disposition: form-data; name="%s"' % key buffer += '\r\n\r\n' + value + '\r\n' buffer += '--%s--\r\n\r\n' % boundary return boundary, buffer multipart_encode = Callable(multipart_encode) https_request = http_request # Nagstamon/Nagstamon/Config.py000066400000000000000000001367711240775040100165320ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import os import platform import sys import ConfigParser import base64 import zlib class Config(object): """ The place for central configuration. """ def __init__(self): """ read config file and set the appropriate attributes supposed to be sensible defaults """ # move from minute interval to seconds self.update_interval_seconds = 60 self.short_display = False self.long_display = True self.show_grid = True self.show_tooltips = True self.highlight_new_events = True self.default_sort_field = "Status" self.default_sort_order = "Descending" self.filter_all_down_hosts = False self.filter_all_unreachable_hosts = False self.filter_all_flapping_hosts = False self.filter_all_unknown_services = False self.filter_all_warning_services = False self.filter_all_critical_services = False self.filter_all_flapping_services = False self.filter_acknowledged_hosts_services = False self.filter_hosts_services_disabled_notifications = False self.filter_hosts_services_disabled_checks = False self.filter_hosts_services_maintenance = False self.filter_services_on_acknowledged_hosts = False self.filter_services_on_down_hosts = False self.filter_services_on_hosts_in_maintenance = False self.filter_services_on_unreachable_hosts = False self.filter_hosts_in_soft_state = False self.filter_services_in_soft_state = False self.position_x = 30 self.position_y = 30 self.popup_details_hover = True self.popup_details_clicking = False self.close_details_hover = True self.close_details_clicking = False self.connect_by_host = True self.connect_by_dns = False self.connect_by_ip = False self.debug_mode = False self.debug_to_file = False self.debug_file = os.path.expanduser('~') + os.sep + "nagstamon.log" self.check_for_new_version = True self.notification = True self.notification_flashing = True self.notification_desktop = False self.notification_actions = False self.notification_sound = True self.notification_sound_repeat = False self.notification_default_sound = True self.notification_custom_sound = False self.notification_custom_sound_warning = None self.notification_custom_sound_critical = None self.notification_custom_sound_down = None self.notification_action_warning = False self.notification_action_warning_string = "" self.notification_action_critical = False self.notification_action_critical_string = "" self.notification_action_down = False self.notification_action_down_string = "" self.notification_action_ok = False self.notification_action_ok_string = "" self.notification_custom_action = False self.notification_custom_action_string = False self.notification_custom_action_separator = False self.notification_custom_action_single = False self.notify_if_warning = True self.notify_if_critical = True self.notify_if_unknown = True self.notify_if_unreachable = True self.notify_if_down = True """ # not yet working # Check_MK show-only-my-problems-they-are-way-enough feature self.only_my_issues = False """ # Regular expression filters self.re_host_enabled = False self.re_host_pattern = "" self.re_host_reverse = False self.re_service_enabled = False self.re_service_pattern = "" self.re_service_reverse = False self.re_status_information_enabled = False self.re_status_information_pattern = "" self.re_status_information_reverse = False self.color_ok_text = self.default_color_ok_text = "#FFFFFF" self.color_ok_background = self.default_color_ok_background = "#006400" self.color_warning_text = self.default_color_warning_text = "#000000" self.color_warning_background = self.default_color_warning_background = "#FFFF00" self.color_critical_text = self.default_color_critical_text = "#FFFFFF" self.color_critical_background = self.default_color_critical_background = "#FF0000" self.color_unknown_text = self.default_color_unknown_text = "#000000" self.color_unknown_background = self.default_color_unknown_background = "#FFA500" self.color_unreachable_text = self.default_color_unreachable_text = "#FFFFFF" self.color_unreachable_background = self.default_color_unreachable_background = "#8B0000" self.color_down_text = self.default_color_down_text = "#FFFFFF" self.color_down_background = self.default_color_down_background = "#000000" self.color_error_text = self.default_color_error_text= "#000000" self.color_error_background = self.default_color_error_background = "#D3D3D3" # going to be obsolete even on Linux #self.statusbar_systray = False self.statusbar_floating = True self.icon_in_systray = False self.appindicator = False self.fullscreen = False self.fullscreen_display = 0 self.systray_popup_offset= 10 self.defaults_acknowledge_sticky = False self.defaults_acknowledge_send_notification = False self.defaults_acknowledge_persistent_comment = False self.defaults_acknowledge_all_services = False self.defaults_acknowledge_comment = "acknowledged" self.defaults_submit_check_result_comment = "check result submitted" self.defaults_downtime_duration_hours = "2" self.defaults_downtime_duration_minutes = "0" self.defaults_downtime_comment = "scheduled downtime" self.defaults_downtime_type_fixed = True self.defaults_downtime_type_flexible = False self.converted_from_single_configfile = False # internal flag to determine if keyring is available at all - defaults to False # use_system_keyring is checked and defined some lines later after config file was read self.keyring_available = False # Special FX # Centreon self.re_criticality_enabled = False self.re_criticality_pattern = "" self.re_criticality_reverse = False # the app is unconfigured by default and will stay so if it # would not find a config file self.unconfigured = True # try to use a given config file - there must be one given # if sys.argv is larger than 1 if len(sys.argv) > 1: # MacOSX related -psn argument by launchd if sys.argv[1].find("-psn") != -1: # new configdir approach self.configdir = os.path.expanduser('~') + os.sep + ".nagstamon" else: # allow to give a config file self.configdir = sys.argv[1] # otherwise if there exits a configfile in current working directory it should be used elif os.path.exists(os.getcwd() + os.sep + "nagstamon.config"): self.configdir = os.getcwd() + os.sep + "nagstamon.config" else: # ~/.nagstamon/nagstamon.conf is the user conf file # os.path.expanduser('~') finds out the user HOME dir where # nagstamon expects its conf file to be self.configdir = os.path.expanduser('~') + os.sep + ".nagstamon" self.configfile = self.configdir + os.sep + "nagstamon.conf" # make path fit for actual os, normcase for letters and normpath for path self.configfile = os.path.normpath(os.path.normcase(self.configfile)) # because the name of the configdir is also stored in the configfile # there may be situations where the name gets overwritten by a # wrong name so it will be stored here temporarily configdir_temp = self.configdir # legacy configfile treatment legacyconfigfile = self._LoadLegacyConfigFile() # default settings dicts self.servers = dict() self.actions = dict() if os.path.exists(self.configfile): # instantiate a Configparser to parse the conf file # SF.net bug #3304423 could be fixed with allow_no_value argument which # is only available since Python 2.7 if sys.version_info[0] < 3 and sys.version_info[1] < 7: config = ConfigParser.ConfigParser() else: config = ConfigParser.ConfigParser(allow_no_value=True) config.read(self.configfile) # temporary dict for string-to-bool-conversion BOOLPOOL = {"False": False, "True": True} # go through all sections of the conf file for section in config.sections(): # go through all items of each sections (in fact there is only on # section which has to be there to comply to the .INI file standard for i in config.items(section): # create a key of every config item with its appropriate value # check first if it is a bool value and convert string if it is if i[1] in BOOLPOOL: object.__setattr__(self, i[0], BOOLPOOL[i[1]]) else: object.__setattr__(self, i[0], i[1]) # because the switch from Nagstamon 1.0 to 1.0.1 brings the use_system_keyring property # and all the thousands 1.0 installations do not know it yet it will be more comfortable # for most of the Windows users if it is only defined as False after it was checked # from config file if not self.__dict__.has_key("use_system_keyring"): if self.unconfigured == True: # an unconfigured system should start with no keyring to prevent crashes self.use_system_keyring = False else: # a configured system seemed to be able to run and thus use system keyring if platform.system() in ["Windows", "Darwin"]: self.use_system_keyring = True else: self.use_system_keyring = self.KeyringAvailable() # reset self.configdir to temporarily saved value in case it differs from # the one read from configfile and so it would fail to save next time self.configdir = configdir_temp # Servers configuration... self.servers = self._LoadServersMultipleConfig() # ... and actions self.actions = self.LoadMultipleConfig("actions", "action", "Action") # seems like there is a config file so the app is not unconfigured anymore self.unconfigured = False # if configfile has been converted from legacy configfile reset it to the new value self.configfile = self.configdir + os.sep + "nagstamon.conf" # flag to be evaluated after gui is initialized and used to show a notice if a legacy config file is used # from command line self.legacyconfigfile_notice = False # in case it exists and it has not been used before read legacy config file once if str(self.converted_from_single_configfile) == "False" and not legacyconfigfile == False: # instantiate a Configparser to parse the conf file # SF.net bug #3304423 could be fixed with allow_no_value argument which # is only available since Python 2.7 if sys.version_info[0] < 3 and sys.version_info[1] < 7: config = ConfigParser.ConfigParser() else: config = ConfigParser.ConfigParser(allow_no_value=True) config.read(legacyconfigfile) # go through all sections of the conf file for section in config.sections(): if section.startswith("Server_"): # create server object for every server server_name = dict(config.items(section))["name"] self.servers[server_name] = Server() # go through all items of each sections for i in config.items(section): self.servers[server_name].__setattr__(i[0], i[1]) # deobfuscate username + password inside a try-except loop # if entries have not been obfuscated yet this action should raise an error # and old values (from nagstamon < 0.9.0) stay and will be converted when next # time saving config try: self.servers[server_name].username = self.DeObfuscate(self.servers[server_name].username) if self.servers[server_name].save_password == "False": self.servers[server_name].password = "" else: self.servers[server_name].password = self.DeObfuscate(self.servers[server_name].password) self.servers[server_name].autologin_key = self.DeObfuscate(self.servers[server_name].autologin_key) self.servers[server_name].proxy_username = self.DeObfuscate(self.servers[server_name].proxy_username) self.servers[server_name].proxy_password = self.DeObfuscate(self.servers[server_name].proxy_password) except: pass elif section == "Nagstamon": # go through all items of each sections (in fact there is only on # section which has to be there to comply to the .INI file standard for i in config.items(section): # create a key of every config item with its appropriate value - but please no legacy config file if not i[0] == "configfile": object.__setattr__(self, i[0], i[1]) # add default actions as examples self.actions.update(self._DefaultActions()) # set flag for config file not being evaluated again self.converted_from_single_configfile = True # of course Nagstamon is configured then self.unconfigured = False # add config dir in place of legacy config file # in case there is a default install use the default config dir if legacyconfigfile == os.path.normpath(os.path.normcase(os.path.expanduser('~') + os.sep + ".nagstamon.conf")): self.configdir = os.path.normpath(os.path.normcase(os.path.expanduser('~') + os.sep + ".nagstamon")) else: self.configdir = legacyconfigfile + ".config" self.configfile = self.configdir + os.sep + "nagstamon.conf" # set flag to show legacy command line config file notice self.legacyconfigfile_notice = True # save converted configuration self.SaveConfig() # Load actions if Nagstamon is not unconfigured, otherwise load defaults if str(self.unconfigured) == "True": self.actions = self._DefaultActions() # do some conversion stuff needed because of config changes and code cleanup self._LegacyAdjustments() def _LoadServersMultipleConfig(self): """ load servers config - special treatment because of obfuscated passwords """ self.keyring_available = self.KeyringAvailable() servers = self.LoadMultipleConfig("servers", "server", "Server") # deobfuscate username + password inside a try-except loop # if entries have not been obfuscated yet this action should raise an error # and old values (from nagstamon < 0.9.0) stay and will be converted when next # time saving config try: for server in servers: # usernames for monitor server and proxy servers[server].username = self.DeObfuscate(servers[server].username) servers[server].proxy_username = self.DeObfuscate(servers[server].proxy_username) # passwords for monitor server and proxy if servers[server].save_password == "False": servers[server].password = "" elif self.keyring_available and self.use_system_keyring: # necessary to import on-the-fly due to possible Windows crashes try: import keyring except: import Nagstamon.thirdparty.keyring as keyring password = keyring.get_password("Nagstamon", "@".join((servers[server].username, servers[server].monitor_url))) or "" if password == "": if servers[server].password != "": servers[server].password = self.DeObfuscate(servers[server].password) else: servers[server].password = password elif servers[server].password != "": servers[server].password = self.DeObfuscate(servers[server].password) # proxy password if self.keyring_available and self.use_system_keyring: # necessary to import on-the-fly due to possible Windows crashes try: import keyring except: import Nagstamon.thirdparty.keyring as keyring proxy_password = keyring.get_password("Nagstamon", "@".join(("proxy", servers[server].proxy_username, servers[server].proxy_address))) or "" if proxy_password == "": if servers[server].proxy_password != "": servers[server].proxy_password = self.DeObfuscate(servers[server].proxy_password) else: servers[server].proxy_password = proxy_password elif servers[server].proxy_password != "": servers[server].proxy_password = self.DeObfuscate(servers[server].proxy_password) # do only deobfuscating if any autologin_key is set - will be only Centreon if servers[server].__dict__.has_key("autologin_key"): if len(servers[server].__dict__["autologin_key"]) > 0: servers[server].autologin_key = self.DeObfuscate(servers[server].autologin_key) except: import traceback traceback.print_exc(file=sys.stdout) return servers def _LoadLegacyConfigFile(self): """ load any pre-0.9.9 config file """ # default negative setting legacyconfigfile = False # try to use a given config file - there must be one given # if sys.argv is larger than 1 if len(sys.argv) > 1: if sys.argv[1].find("-psn") != -1: legacyconfigfile = os.path.expanduser('~') + os.sep + ".nagstamon.conf" else: # allow to give a config file legacyconfigfile = sys.argv[1] # otherwise if there exits a configfile in current working directory it should be used elif os.path.exists(os.getcwd() + os.sep + "nagstamon.conf"): legacyconfigfile = os.getcwd() + os.sep + "nagstamon.conf" else: # ~/.nagstamon.conf is the user conf file # os.path.expanduser('~') finds out the user HOME dir where # nagstamon expects its conf file to be legacyconfigfile = os.path.expanduser('~') + os.sep + ".nagstamon.conf" # make path fit for actual os, normcase for letters and normpath for path legacyconfigfile = os.path.normpath(os.path.normcase(legacyconfigfile)) if os.path.exists(legacyconfigfile) and os.path.isfile(legacyconfigfile): return legacyconfigfile else: return False def LoadMultipleConfig(self, settingsdir, setting, configobj): """ load generic config into settings dict and return to central config """ # defaults as empty dict in case settings dir/files could not be found settings = dict() try: if os.path.exists(self.configdir + os.sep + settingsdir): # dictionary that later gets returned back settings = dict() for f in os.listdir(self.configdir + os.sep + settingsdir): if f.startswith(setting + "_") and f.endswith(".conf"): if sys.version_info[0] < 3 and sys.version_info[1] < 7: config = ConfigParser.ConfigParser() else: config = ConfigParser.ConfigParser(allow_no_value=True) config.read(self.configdir + os.sep + settingsdir + os.sep + f) # create object for every setting name = f.split("_", 1)[1].rpartition(".")[0] settings[name] = globals()[configobj]() # go through all items of the server for i in config.items(setting + "_" + name): # create a key of every config item with its appropriate value settings[name].__setattr__(i[0], i[1]) except: import traceback traceback.print_exc(file=sys.stdout) return settings def SaveConfig(self, output=None, server=None, debug_queue=None): """ save config file "output", "server" and debug_queue are used only for debug purpose - which one is given will be taken """ try: # Make sure .nagstamon is created if not os.path.exists(self.configdir): os.mkdir(self.configdir) # save config file with ConfigParser config = ConfigParser.ConfigParser() # general section for Nagstamon config.add_section("Nagstamon") for option in self.__dict__: if not option in ["servers", "actions"]: config.set("Nagstamon", option, self.__dict__[option]) # because the switch from Nagstamon 1.0 to 1.0.1 brings the use_system_keyring property # and all the thousands 1.0 installations do not know it yet it will be more comfortable # for most of the Windows users if it is only defined as False after it was checked # from config file if not self.__dict__.has_key("use_system_keyring"): if self.unconfigured == True: # an unconfigured system should start with no keyring to prevent crashes self.use_system_keyring = False else: # a configured system seemed to be able to run and thus use system keyring if platform.system() in ["Windows", "Darwin"]: self.use_system_keyring = True else: self.use_system_keyring = self.KeyringAvailable() # save servers dict self.SaveMultipleConfig("servers", "server") # save actions dict self.SaveMultipleConfig("actions", "action") # debug if str(self.debug_mode) == "True": if server != None: server.Debug(server="", debug="Saving config to " + self.configfile) elif output != None: output.servers.values()[0].Debug(server="", debug="Saving config to " + self.configfile) # open, save and close config file f = open(os.path.normpath(self.configfile), "w") config.write(f) f.close() except Exception, err: print err import traceback traceback.print_exc(file=sys.stdout) # debug if str(self.debug_mode) == "True": if server != None: server.Debug(server="", debug="Saving config to " + self.configfile) elif output != None: output.servers.values()[0].Debug(server="", debug="Saving config to " + self.configfile) elif debug_queue != None: debug_string = " ".join((head + ":", str(datetime.datetime.now()), "Saving config to " + self.configfile)) # give debug info to debug loop for thread-save log-file writing self.debug_queue.put(debug_string) def SaveMultipleConfig(self, settingsdir, setting): """ saves conf files for settings like actions in extra directories "multiple" means that multiple confs for actions or servers are loaded, not just one like for e.g. sound file """ # only import keyring lib if configured to do so - to avoid Windows crashes # like https://github.com/HenriWahl/Nagstamon/issues/97 if self.use_system_keyring == True: self.keyring_available = self.KeyringAvailable() # one section for each setting for s in self.__dict__[settingsdir]: # depending on python version allow_no_value is allowed or not if sys.version_info[0] < 3 and sys.version_info[1] < 7: config = ConfigParser.ConfigParser() else: config = ConfigParser.ConfigParser(allow_no_value=True) config.add_section(setting + "_" + s) for option in self.__dict__[settingsdir][s].__dict__: # obfuscate certain entries in config file - special arrangement for servers if settingsdir == "servers": #if option == "username" or option == "password" or option == "proxy_username" or option == "proxy_password" or option == "autologin_key": if option in ["username", "password", "proxy_username", "proxy_password", "autologin_key"]: value = self.Obfuscate(self.__dict__[settingsdir][s].__dict__[option]) if option == "password": if self.__dict__[settingsdir][s].save_password == "False": value = "" elif self.keyring_available and self.use_system_keyring: if self.__dict__[settingsdir][s].password != "": # necessary to import on-the-fly due to possible Windows crashes try: import keyring except: import Nagstamon.thirdparty.keyring as keyring # provoke crash if password saving does not work - this is the case # on newer Ubuntu releases try: keyring.set_password("Nagstamon", "@".join((self.__dict__[settingsdir][s].username, self.__dict__[settingsdir][s].monitor_url)), self.__dict__[settingsdir][s].password) except: import traceback traceback.print_exc(file=sys.stdout) sys.exit(1) value = "" if option == "proxy_password": if self.keyring_available and self.use_system_keyring: # necessary to import on-the-fly due to possible Windows crashes try: import keyring except: import Nagstamon.thirdparty.keyring as keyring if self.__dict__[settingsdir][s].proxy_password != "": # provoke crash if password saving does not work - this is the case # on newer Ubuntu releases try: keyring.set_password("Nagstamon", "@".join(("proxy",\ self.__dict__[settingsdir][s].proxy_username, self.__dict__[settingsdir][s].proxy_address)), self.__dict__[settingsdir][s].proxy_password) except: import traceback traceback.print_exc(file=sys.stdout) sys.exit(1) value = "" config.set(setting + "_" + s, option, value) else: config.set(setting + "_" + s, option, self.__dict__[settingsdir][s].__dict__[option]) else: config.set(setting + "_" + s, option, self.__dict__[settingsdir][s].__dict__[option]) # open, save and close config_server file if not os.path.exists(self.configdir + os.sep + settingsdir): os.mkdir(self.configdir + os.sep + settingsdir) f = open(os.path.normpath(self.configdir + os.sep + settingsdir + os.sep + setting + "_" + s + ".conf"), "w") config.write(f) f.close() # clean up old deleted/renamed config files if os.path.exists(self.configdir + os.sep + settingsdir): for f in os.listdir(self.configdir + os.sep + settingsdir): if not f.split(setting + "_")[1].split(".conf")[0] in self.__dict__[settingsdir]: os.unlink(self.configdir + os.sep + settingsdir + os.sep + f) def Convert_Conf_to_Multiple_Servers(self): """ if there are settings found which come from older nagstamon version convert them - now with multiple servers support these servers have their own settings DEPRECATED I think, after 2,5 years have passed there should be no version less than 0.8.0 in the wild... """ # check if old settings exist if self.__dict__.has_key("nagios_url") and \ self.__dict__.has_key("nagios_cgi_url") and \ self.__dict__.has_key("username") and \ self.__dict__.has_key("password") and \ self.__dict__.has_key("use_proxy_yes") and \ self.__dict__.has_key("use_proxy_no"): # create Server and fill it with old settings server_name = "Default" self.servers[server_name] = Server() self.servers[server_name].name = server_name self.servers[server_name].monitor_url = self.nagios_url self.servers[server_name].monitor_cgi_url = self.nagios_cgi_url self.servers[server_name].username = self.username self.servers[server_name].password = self.password # convert VERY old config files try: self.servers[server_name].use_proxy = self.use_proxy_yes except: self.servers[server_name].use_proxy = False try: self.servers[server_name].use_proxy_from_os = self.use_proxy_from_os_yes except: self.servers[server_name].use_proxy_from_os = False # delete old settings from config self.__dict__.pop("nagios_url") self.__dict__.pop("nagios_cgi_url") self.__dict__.pop("username") self.__dict__.pop("password") self.__dict__.pop("use_proxy_yes") self.__dict__.pop("use_proxy_no") def KeyringAvailable(self): """ determine if keyring module and an implementation is available for secure password storage """ try: # Linux systems should use keyring only if it comes with the distro, otherwise chances are small # that keyring works at all if not platform.system() in ["Windows", "Darwin"]: # keyring and secretstorage have to be importable import keyring, secretstorage if ("SecretService") in dir(keyring.backends) and not (keyring.get_keyring() is None): return True else: # safety first - if not yet available disable it if not self.__dict__.has_key("use_system_keyring"): self.use_system_keyring = False # only import keyring lib if configured to do so # necessary to avoid Windows crashes like https://github.com/HenriWahl/Nagstamon/issues/97 if self.use_system_keyring == True: # hint for packaging: nagstamon.spec always have to match module path # keyring has to be bound to object to be used later import Nagstamon.thirdparty.keyring as keyring return not (keyring.get_keyring() is None) else: return False except: import traceback traceback.print_exc(file=sys.stdout) return False def Convert_Conf_to_Custom_Actions(self): """ any nagstamon minor to 0.9.9 will have extra ssh/rdp/vnc settings which will be converted to custom actions here """ # check if old settings exist if self.__dict__.has_key("app_ssh_bin") and \ self.__dict__.has_key("app_ssh_options") and \ self.__dict__.has_key("app_rdp_bin") and \ self.__dict__.has_key("app_rdp_options") and \ self.__dict__.has_key("app_vnc_bin") and \ self.__dict__.has_key("app_vnc_options"): # create actions and fill them with old settings self.actions["SSH"] = Action(name="SSH", type="command", description="Converted from pre 0.9.9 Nagstamon.", string=self.app_ssh_bin + " " + self.app_ssh_options + " $ADDRESS$") self.actions["RDP"] = Action(name="RDP", type="command", description="Converted from pre 0.9.9 Nagstamon.", string=self.app_rdp_bin + " " + self.app_rdp_options + " $ADDRESS$") self.actions["VNC"] = Action(name="VNC", type="command", description="Converted from pre 0.9.9 Nagstamon.", string=self.app_vnc_bin + " " + self.app_vnc_options + " $ADDRESS$") # delete old settings from config self.__dict__.pop("app_ssh_bin") self.__dict__.pop("app_ssh_options") self.__dict__.pop("app_rdp_bin") self.__dict__.pop("app_rdp_options") self.__dict__.pop("app_vnc_bin") self.__dict__.pop("app_vnc_options") def Obfuscate(self, string, count=5): """ Obfuscate a given string to store passwords etc. """ for i in range(count): string = list(base64.b64encode(string)) string.reverse() string = "".join(string) string = zlib.compress(string) string = base64.b64encode(string) return string def DeObfuscate(self, string, count=5): string = base64.b64decode(string) for i in range(count): string = zlib.decompress(string) string = list(string) string.reverse() string = "".join(string) string = base64.b64decode(string) return string def _DefaultActions(self): """ create some default actions like SSH and so on """ if platform.system() == "Windows": defaultactions = { "RDP": Action(name="RDP", description="Connect via RDP.", type="command", string="C:\windows\system32\mstsc.exe $ADDRESS$"), "VNC": Action(name="VNC", description="Connect via VNC.", type="command", string="C:\Program Files\TightVNC\vncviewer.exe $ADDRESS$"), "Telnet": Action(name="Telnet", description="Connect via Telnet.", type="command", string="C:\Windows\System32\Telnet.exe root@$ADDRESS$"), "SSH": Action(name="SSH", description="Connect via SSH.", type="command", string="C:\Program Files\PuTTY\putty.exe -l root $ADDRESS$") } elif platform.system() == "Darwin": defaultactions = { "RDP": Action(name="RDP", description="Connect via RDP.", type="command", string="open rdp://$ADDRESS$"), "VNC": Action(name="VNC", description="Connect via VNC.", type="command", string="open vnc://$ADDRESS$"), "SSH": Action(name="SSH", description="Connect via SSH.", type="command", string="open ssh://root@$ADDRESS$"), "Telnet": Action(name="Telnet", description="Connect via Telnet.", type="command", string="open telnet://root@$ADDRESS$") } else: # the Linux settings defaultactions = { "RDP": Action(name="RDP", description="Connect via RDP.", type="command", string="/usr/bin/rdesktop -g 1024x768 $ADDRESS$"), "VNC": Action(name="VNC", description="Connect via VNC.", type="command", string="/usr/bin/vncviewer $ADDRESS$"), "SSH": Action(name="SSH", description="Connect via SSH.", type="command", string="/usr/bin/gnome-terminal -x ssh root@$ADDRESS$"), "Telnet": Action(name="Telnet", description="Connect via Telnet.", type="command", string="/usr/bin/gnome-terminal -x telnet root@$ADDRESS$"), "Update-Linux": Action(name="Update-Linux", description="Run remote update script.", type="command", string="/usr/bin/terminator -x ssh root@$HOST$ update.sh", enabled=False) } # OS agnostic actions as examples defaultactions["Nagios-1-Click-Acknowledge-Host"] = Action(name="Nagios-1-Click-Acknowledge-Host", type="url", description="Acknowledges a host with one click.", filter_target_service=False, enabled=False, string="$MONITOR-CGI$/cmd.cgi?cmd_typ=33&cmd_mod=2&host=$HOST$\ &com_author=$USERNAME$&com_data=acknowledged&btnSubmit=Commit") defaultactions["Nagios-1-Click-Acknowledge-Service"] = Action(name="Nagios-1-Click-Acknowledge-Service", type="url", description="Acknowledges a service with one click.", filter_target_host=False, enabled=False, string="$MONITOR-CGI$/cmd.cgi?cmd_typ=34&cmd_mod=2&host=$HOST$\ &service=$SERVICE$&com_author=$USERNAME$&com_data=acknowledged&btnSubmit=Commit") defaultactions["Opsview-Graph-Service"] = Action(name="Opsview-Graph-Service", type="browser", description="Show graph in browser.", filter_target_host=False, string="$MONITOR$/graph?service=$SERVICE$&host=$HOST$", enabled=False) defaultactions["Opsview-History-Host"] = Action(name="Opsview-Host-Service", type="browser", description="Show host in browser.", filter_target_host=True, string="$MONITOR$/event?host=$HOST$", enabled=False) defaultactions["Opsview-History-Service"] = Action(name="Opsview-History-Service", type="browser", description="Show history in browser.", filter_target_host=True, string="$MONITOR$/event?host=$HOST$&service=$SERVICE$", enabled=False) defaultactions["Check_MK-1-Click-Acknowledge-Host"] = Action(name="Check_MK-1-Click-Acknowledge-Host", type="url", description="Acknowledges a host with one click.", filter_target_service=False, enabled=False, string="$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=hoststatus&host=$HOST$&_ack_comment=$COMMENT-ACK$&_acknowledge=Acknowledge") defaultactions["Check_MK-1-Click-Acknowledge-Service"] = Action(name="Check_MK-1-Click-Acknowledge-Service", type="url", description="Acknowledges a host with one click.", filter_target_host=False, enabled=False, string="$MONITOR$/view.py?_transid=$TRANSID$&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=service&host=$HOST$&_ack_comment=$COMMENT-ACK$&_acknowledge=Acknowledge&service=$SERVICE$") defaultactions["Check_MK Edit host in WATO"] = Action(name="Check_MK Edit host in WATO", enabled=False, monitor_type="Check_MK Multisite", description="Edit host in WATO.", string="$MONITOR$/index.py?start_url=%2Fmonitor%2Fcheck_mk%2Fwato.py%3Fmode%3Dedithost%26host%3D$HOST$") defaultactions["Email"] = Action(name="Email", enabled=False, description="Send email to someone.", type="browser", string="mailto:servicedesk@my.org?subject=Monitor alert: $HOST$ - $SERVICE$ - $STATUS-INFO$&body=Please help!.%0d%0aBest regards from Nagstamon") return defaultactions def _LegacyAdjustments(self): # mere cosmetics but might be more clear for future additions - changing any "nagios"-setting to "monitor" for s in self.servers.values(): if s.__dict__.has_key("nagios_url"): s.monitor_url = s.nagios_url if s.__dict__.has_key("nagios_cgi_url"): s.monitor_cgi_url = s.nagios_cgi_url # to reduce complexity in Centreon there is also only one URL necessary if s.type == "Centreon": s.monitor_url = s.monitor_cgi_url # switch to update interval in seconds not minutes if self.__dict__.has_key("update_interval"): self.update_interval_seconds = int(self.update_interval) * 60 self.__dict__.pop("update_interval") # remove support for GNOME2-trayicon-egg-stuff if self.__dict__.has_key("statusbar_systray"): if str(self.statusbar_systray) == "True": self.icon_in_systray = "True" self.__dict__.pop("statusbar_systray") def GetNumberOfEnabledMonitors(self): """ returns the number of enabled monitors - in case all are disabled there is no need to display the popwin """ # to be returned number = 0 for server in self.servers.values(): if str(server.enabled) == "True": number += 1 return number class Server(object): """ one Server realized as object for config info """ def __init__(self): self.enabled = True self.type = "Nagios" self.name = "" self.monitor_url = "" self.monitor_cgi_url = "" self.username = "" self.password = "" self.save_password = True self.use_proxy = False self.use_proxy_from_os = False self.proxy_address = "" self.proxy_username = "" self.proxy_password = "" # special FX # Centreon autologin self.use_autologin = False self.autologin_key = "" # Icinga "host_display_name" instead of "host" self.use_display_name_host = False self.use_display_name_service = False class Action(object): """ class for custom actions, which whill be thrown into one config dictionary like the servers """ def __init__(self, **kwds): # to be or not to be enabled... self.enabled = True # monitor type self.monitor_type = "" # one of those: browser, url or command self.type = "browser" # thy name is... self.name = "Custom action" # OS of host where Nagstamon runs - especially commands are mostly not platform agnostic self.os = "" # description self.description = "Starts a custom action." # might be URL in case of type browser/url and a commandline for commands self.string = "" # version - maybe in future this might be more sophisticated self.version = "1" # kind of Nagios item this action is targeted to - maybe also usable for states self.filter_target_host = True self.filter_target_service = True # action applies only to certain hosts or services self.re_host_enabled = False self.re_host_pattern = "" self.re_host_reverse = False self.re_service_enabled = False self.re_service_pattern = "" self.re_service_reverse = False self.re_status_information_enabled = False self.re_status_information_pattern = "" self.re_status_information_reverse = False # close powin or not, depends on personal preference self.close_popwin = True self.leave_popwin_open = False # special FX # Centreon criticality and autologin self.re_criticality_enabled = False self.re_criticality_pattern = "" self.re_criticality_reverse = False # add and/or all keywords to object for k in kwds: self.__dict__[k] = kwds[k] Nagstamon/Nagstamon/Custom.py000066400000000000000000000034161240775040100165640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 """ Module for implementing custom servers, columns and other stuff. Imported in GUI module. """ from Nagstamon.Actions import register_server from Nagstamon.Server.Nagios import NagiosServer from Nagstamon.Server.Centreon import CentreonServer from Nagstamon.Server.Icinga import IcingaServer from Nagstamon.Server.Multisite import MultisiteServer from Nagstamon.Server.op5Monitor import Op5MonitorServer from Nagstamon.Server.Opsview import OpsviewServer from Nagstamon.Server.Thruk import ThrukServer from Nagstamon.Server.Zabbix import ZabbixServer # moved registration process because of circular dependencies # order of registering affects sorting in server type list in add new server dialog register_server(NagiosServer) register_server(CentreonServer) register_server(MultisiteServer) register_server(IcingaServer) register_server(Op5MonitorServer) register_server(OpsviewServer) register_server(ThrukServer) register_server(ZabbixServer) Nagstamon/Nagstamon/GUI.py000066400000000000000000007165611240775040100157520ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 try: import pygtk pygtk.require("2.0") except Exception, err: print print err print print "Could not load pygtk, maybe you need to install python gtk." print import sys sys.exit() import gtk import gobject import os import platform import sys import copy # testing pynotify support try: import pynotify pynotify.init("Nagstamon") except: pass # testing Ubuntu AppIndicator support try: import appindicator except: pass # needed for actions e.g. triggered by pressed buttons from Nagstamon import Config from Nagstamon import Actions from Nagstamon import Custom class Sorting(object): """ Sorting persistence purpose class Stores tuple pairs in form of: (, 0: if length >= self.max_remember: self.sorting_tuple_list.pop() if id == self.sorting_tuple_list[0][0]: self.sorting_tuple_list.remove(self.sorting_tuple_list[0]) self.sorting_tuple_list.insert(0, (id, order)) class GUI(object): """ class which organizes the GUI """ def __init__(self, **kwds): """ some fundamental preliminaries """ # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # Meta self.name = "Nagstamon" self.version = "1.0.1" self.website = "https://nagstamon.ifw-dresden.de/" self.copyright = "©2008-2014 Henri Wahl et al.\nh.wahl@ifw-dresden.de" self.comments = "Nagios status monitor for your desktop" # initialize overall status flag self.status_ok = True # if run first it is impossible to refresh the display with # non-existent settings so there has to be extra treatment # at the second run nagstamon will be configured and so no first run if self.conf.unconfigured: self.firstrun = True else: self.firstrun = False # font size, later adjusted by StatusBar self.fontsize = 10000 # store information about monitors self.monitors = dict() self.current_monitor = 0 # define colors for detailed status table in dictionaries self.TAB_BG_COLORS = { "UNKNOWN":str(self.conf.color_unknown_background), "CRITICAL":str(self.conf.color_critical_background), "WARNING":str(self.conf.color_warning_background), "DOWN":str(self.conf.color_down_background), "UNREACHABLE":str(self.conf.color_unreachable_background) } self.TAB_FG_COLORS = { "UNKNOWN":str(self.conf.color_unknown_text), "CRITICAL":str(self.conf.color_critical_text), "WARNING":str(self.conf.color_warning_text), "DOWN":str(self.conf.color_down_text), "UNREACHABLE":str(self.conf.color_unreachable_text) } # define popwin table liststore types self.LISTSTORE_COLUMNS = [gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING,\ gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING,\ gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING,\ gobject.TYPE_STRING, gobject.TYPE_STRING,\ gtk.gdk.Pixbuf, gtk.gdk.Pixbuf, gtk.gdk.Pixbuf, gtk.gdk.Pixbuf,\ gtk.gdk.Pixbuf, gtk.gdk.Pixbuf, gtk.gdk.Pixbuf, gtk.gdk.Pixbuf,\ gtk.gdk.Pixbuf, gtk.gdk.Pixbuf] # decide if the platform can handle SVG if not use PNG if platform.system() in ["Darwin", "Windows"]: self.BitmapSuffix = ".png" else: self.BitmapSuffix = ".svg" # set app icon for all app windows gtk.window_set_default_icon_from_file(self.Resources + os.sep + "nagstamon" + self.BitmapSuffix) if platform.system() == "Darwin": # MacOSX gets instable with default theme "Clearlooks" so use custom one with theme "Murrine" gtk.rc_parse_string('gtk-theme-name = "Murrine"') # workaround for ugly Fonts on Maverick if platform.release() > "12": gtk.rc_parse_string('style "font" {font_name = "Lucida Grande"} widget_class "*" style "font"') # init MacOSX integration import gtk_osxapplication osxapp = gtk_osxapplication.OSXApplication() # prevent blocking osxapp.connect("NSApplicationBlockTermination", gtk.main_quit) osxapp.ready() # icons for acknowledgement/downtime visualization self.STATE_ICONS = dict() for icon in ["fresh", "acknowledged", "downtime", "flapping", "passive"]: self.STATE_ICONS[icon] = gtk.gdk.pixbuf_new_from_file_at_size(self.Resources\ + os.sep + "nagstamon_" + icon + self.BitmapSuffix,\ int(self.fontsize/650), int(self.fontsize/650)) # Icon in systray and statusbar both get created but # only one of them depending on the settings will # be shown self.statusbar = StatusBar(conf=self.conf, output=self) # Popup is a WINDOW_POPUP without border etc. self.popwin = Popwin(conf=self.conf, output=self) # find out dimension of all monitors for m in range(self.statusbar.StatusBar.get_screen().get_n_monitors()): monx0, mony0, monw, monh = self.statusbar.StatusBar.get_screen().get_monitor_geometry(m) self.monitors[m] = (monx0, mony0, monw, monh) # testing Ubuntu AppIndicator if sys.modules.has_key("appindicator"): self.appindicator = AppIndicator(conf=self.conf, output=self) # check if statusbar is inside display boundaries # give 5 pixels tolerance to x0 and y0 # modify x0 and y0 to fit into display statusbar_x0, statusbar_y0 = self.statusbar.StatusBar.get_position() m = self.statusbar.StatusBar.get_screen().get_monitor_at_point(statusbar_x0, statusbar_y0) # get max dimensions of current display x0, y0, x_max, y_max = self._get_display_dimensions(m) if not (x0-5 <= int(self.conf.position_x)): self.conf.position_x = x0 + 30 if not int(self.conf.position_x) <= x_max: self.conf.position_x = x_max - 50 if not (y0-5 <= int(self.conf.position_y)): self.conf.position_y = y0 + 30 if not int(self.conf.position_y) <= y_max: self.conf.position_y = y_max - 50 self.statusbar.StatusBar.move(int(self.conf.position_x), int(self.conf.position_y)) if str(self.conf.fullscreen) == "True": self.popwin.Window.show_all() self.popwin.Window.set_visible(True) self.popwin.RefreshFullscreen() else: self.popwin.Resize() # connect events to actions # when talking about "systray" the Windows variant of upper left desktop corner # statusbar is meant synonymical # if pointer on systray do popup the long-summary-status-window aka popwin self.statusbar.SysTray.connect("activate", self.statusbar.SysTrayClicked) self.statusbar.SysTray.connect("popup-menu", self.statusbar.MenuPopup) # if pointer clicks on logo move stautsbar self.statusbar.LogoEventbox.connect("button-press-event", self.statusbar.LogoClicked) self.statusbar.LogoEventbox.connect("button-release-event", self.statusbar.LogoReleased) # if pointer hovers or clicks statusbar show details self.statusbar.EventBoxLabel.connect("enter-notify-event", self.statusbar.Hovered) self.statusbar.EventBoxLabel.connect("button-press-event", self.statusbar.Clicked) # server combobox self.popwin.ComboboxMonitor.connect("changed", self.popwin.ComboboxClicked) # attempt to place and resize statusbar where it belongs to in Windows - workaround self.statusbar.StatusBar.move(int(self.conf.position_x), int(self.conf.position_y)) self.statusbar.Resize() # flag which is set True if already notifying self.Notifying = False # last worst state for notification self.last_worst_status = "UP" # defining sorting defaults in first render self.COLUMNS_IDS_MAP = {"Host": 0,\ "Service": 1,\ "Status": 2,\ "Last Check": 3,\ "Duration": 4,\ "Attempt": 5,\ "Status Information": 6} # reverse mapping of column names and IDs for settings dialog self.IDS_COLUMNS_MAP = dict((id, column) for column, id in self.COLUMNS_IDS_MAP.iteritems()) # use configured default sorting order if str(self.conf.default_sort_order) == "Ascending": self.startup_sort_order = gtk.SORT_ASCENDING else: self.startup_sort_order = gtk.SORT_DESCENDING self.startup_sort_field = self.COLUMNS_IDS_MAP[self.conf.default_sort_field] self.rows_reordered_handler = {} self.last_sorting = {} for server in self.servers.values(): self.last_sorting[server.get_name()] = Sorting([(self.startup_sort_field, self.startup_sort_order ), (server.HOST_COLUMN_ID, gtk.SORT_ASCENDING)], len(server.COLUMNS)+1) # stores sorting between table refresh # store once created dialogs here to minimize memory usage self.Dialogs = {} # history of events to track status changes for notifications # events that came in self.events_current = {} # events that had been already displayed in popwin and need no extra mark self.events_history = {} # events to be given to custom notification, maybe to desktop notification too self.events_notification = {} def _get_display_dimensions(self, monitor): """ get x0 y0 xmax and ymax of a distinct monitor, usefull to put statusbar inside the fence """ # start with values of first display x0, y0, x_max, y_max = self.monitors[0] # only calculate some dimensions when multiple displays are used if len(self.monitors) > 1: # run through all displays for m in range(1, monitor+1): # if 2 displays have some coordinates in common they belong to each other if self.monitors[m-1][2] == self.monitors[m][0]: x0 += self.monitors[m][0] x_max += self.monitors[m][2] else: x0 = self.monitors[m][0] x_max = self.monitors[m][2] + self.monitors[m][0] if self.monitors[m-1][3] == self.monitors[m][1]: y0 += self.monitors[m][1] y_max += self.monitors[m][3] else: y0 = self.monitors[m][1] y_max = self.monitors[m][3] + self.monitors[m][1] return x0, y0, x_max, y_max def get_last_sorting(self, server): return self.last_sorting[server.get_name()] def get_rows_reordered_handler(self, server): return self.rows_reordered_handler.get(server.get_name()) def set_rows_reordered_handler(self, server, handler): self.rows_reordered_handler[server.get_name()] = handler def set_sorting(self, liststore, server): """ Restores sorting after refresh """ for id, order in self.get_last_sorting(server).iteritems(): liststore.set_sort_column_id(id, order) # this makes sorting arrows visible according to # sort order after refresh column = self.popwin.ServerVBoxes[server.get_name()].TreeView.get_column(id) if column is not None: column.set_property('sort-order', order) def on_column_header_click(self, model, id, liststore, server): """ Sets current sorting according to column id """ # makes column headers sortable by first time click (hack) order = model.get_sort_order() liststore.set_sort_column_id(id, order) rows_reordered_handler = self.get_rows_reordered_handler(server) if rows_reordered_handler is not None: liststore.disconnect(rows_reordered_handler) new_rows_reordered_handler = liststore.connect_after('rows-reordered', self.on_sorting_order_change, id, model, server) self.set_rows_reordered_handler(server, new_rows_reordered_handler) else: new_rows_reordered_handler = liststore.connect_after('rows-reordered', self.on_sorting_order_change, id, model, server, False) self.set_rows_reordered_handler(server, new_rows_reordered_handler) self.on_sorting_order_change(liststore, None, None, None, id, model, server) model.set_sort_column_id(id) def on_sorting_order_change(self, liststore, path, iter, new_order, id, model, server, do_action=True): """ Saves current sorting change in object property """ if do_action: order = model.get_sort_order() last_sorting = self.get_last_sorting(server) last_sorting.add(id, order) def RefreshDisplayStatus(self): """ load current nagios status and refresh trayicon and detailed treeview add only services which are not on maintained or acknowledged hosts this way applying the nagios filter more comfortably because in nagios one had to schedule/acknowledge every single service """ # refresh statusbar # flag for overall status, needed by popwin.popup to decide if popup in case all is OK self.status_ok = False # set handler to None for do not disconnecting it after display refresh self.rows_reordered_handler = {} # local counters for summarize all miserable hosts downs = 0 unreachables = 0 unknowns = 0 criticals = 0 warnings = 0 # display "ERROR" in case of startup connection trouble errors = "" # walk through all servers, RefreshDisplayStatus their hosts and their services for server in self.servers.values(): # only refresh monitor server output if enabled and only once every server loop if str(self.conf.servers[server.get_name()].enabled) == "True" or\ server.refresh_authentication == True: try: # otherwise it must be shown, full of problems self.popwin.ServerVBoxes[server.get_name()].show() self.popwin.ServerVBoxes[server.get_name()].set_visible(True) self.popwin.ServerVBoxes[server.get_name()].set_no_show_all(False) # if needed show auth line # Centreon autologin could be set in Settings dialog if server.refresh_authentication == True and str(server.use_autologin) == "False": self.popwin.ServerVBoxes[server.get_name()].HBoxAuth.set_no_show_all(False) self.popwin.ServerVBoxes[server.get_name()].HBoxAuth.show_all() if self.popwin.ServerVBoxes[server.get_name()].AuthEntryUsername.get_text() == "": self.popwin.ServerVBoxes[server.get_name()].AuthEntryUsername.set_text(server.username) if self.popwin.ServerVBoxes[server.get_name()].AuthEntryPassword.get_text() == "": self.popwin.ServerVBoxes[server.get_name()].AuthEntryPassword.set_text(server.password) else: # no re-authentication necessary self.popwin.ServerVBoxes[server.get_name()].HBoxAuth.hide_all() self.popwin.ServerVBoxes[server.get_name()].HBoxAuth.set_no_show_all(True) # use a bunch of filtered nagitems, services and hosts sorted by different # grades of severity # summarize states downs += server.downs unreachables += server.unreachables unknowns += server.unknowns criticals += server.criticals warnings += server.warnings # if there is no trouble... if len(server.nagitems_filtered["hosts"]["DOWN"]) == 0 and \ len(server.nagitems_filtered["hosts"]["UNREACHABLE"]) == 0 and \ len(server.nagitems_filtered["services"]["CRITICAL"]) == 0 and \ len(server.nagitems_filtered["services"]["WARNING"]) == 0 and \ len(server.nagitems_filtered["services"]["UNKNOWN"]) == 0 and \ server.status_description == "": # ... there is no need to show a label or treeview... self.popwin.ServerVBoxes[server.get_name()].hide() self.popwin.ServerVBoxes[server.get_name()].set_visible(False) self.popwin.ServerVBoxes[server.get_name()].set_no_show_all(True) self.status_ok = True else: # otherwise it must be shown, full of problems self.popwin.ServerVBoxes[server.get_name()].set_visible(True) self.popwin.ServerVBoxes[server.get_name()].set_no_show_all(False) self.popwin.ServerVBoxes[server.get_name()].show_all() self.status_ok = False # calculate freshness of hosts # first reset all events self.events_current.clear() # run through all servers and hosts and services for s in self.servers.values(): for host in s.hosts.values(): if not host.status == "UP": # only if host is not filtered out add it to current events # the boolean is meaningless for current events if host.visible: self.events_current[host.get_hash()] = True for service in host.services.values(): # same for services of host if service.visible: self.events_current[service.get_hash()] = True # check if some cached event still is relevant - kick it out if not for event in self.events_history.keys(): if not event in self.events_current.keys(): self.events_history.pop(event) self.events_notification.pop(event) # if some current event is not yet in event cache add it and mark it as fresh (=True) for event in self.events_current.keys(): if not event in self.events_history.keys() and str(self.conf.highlight_new_events) == "True": self.events_history[event] = True self.events_notification[event] = True # use a liststore for treeview where the table headers all are strings - first empty it # now added with some simple repair after settings dialog has been used # because sometimes after settings changes ListStore and TreeView become NoneType # would be more logical to do this in Actions.CreateServer() but this gives a segfault :-( if not type(server.ListStore) == type(None): server.ListStore.clear() else: server.ListStore = gtk.ListStore(*self.LISTSTORE_COLUMNS) if type(server.TreeView) == type(None): # if treeview got lost recycle the one in servervbox server.TreeView = self.popwin.ServerVBoxes[server.get_name()].TreeView # apart from status informations there we need two columns which # hold the color information, which is derived from status which # is used as key at the above color dictionaries # Update: new columns added which contain pixbufs of flag indicators if needed for item_type, status_dict in server.nagitems_filtered.iteritems(): for status, item_list in status_dict.iteritems(): for single_item in list(item_list): # use copy to fight memory leak item = copy.deepcopy(single_item) line = list(server.get_columns(item)) line.append("%s: %s\n%s" %((line[0], line[1], line[6]))) line.append(self.TAB_FG_COLORS[item.status]) line.append(self.TAB_BG_COLORS[item.status]) # add a slightly changed version of bg_color for better recognition in treeview color = gtk.gdk.color_parse(self.TAB_BG_COLORS[item.status]) color = gtk.gdk.Color(red = self._GetAlternateColor(color.red),\ green = self._GetAlternateColor(color.green),\ blue = self._GetAlternateColor(color.blue),\ pixel = color.pixel) line.append(color.to_string()) # icons for hosts if item.is_host(): if item.get_hash() in self.events_history and self.events_history[item.get_hash()] == True: line.append(self.STATE_ICONS["fresh"]) else: line.append(None) if item.is_acknowledged(): line.append(self.STATE_ICONS["acknowledged"]) else: line.append(None) if item.is_in_scheduled_downtime(): line.append(self.STATE_ICONS["downtime"]) else: line.append(None) if item.is_flapping(): line.append(self.STATE_ICONS["flapping"]) else: line.append(None) if item.is_passive_only(): line.append(self.STATE_ICONS["passive"]) else: line.append(None) # fill line with dummmy values because there will # be none for services if this is a host line.extend([None, None, None, None, None]) # icons for services else: # if the hosting host of a service has any flags display them too # a fresh service's host does not need a freshness icon line.append(None) if server.hosts[item.host].is_acknowledged(): line.append(self.STATE_ICONS["acknowledged"]) else: line.append(None) if server.hosts[item.host].is_in_scheduled_downtime(): line.append(self.STATE_ICONS["downtime"]) else: line.append(None) if server.hosts[item.host].is_flapping(): line.append(self.STATE_ICONS["flapping"]) else: line.append(None) if server.hosts[item.host].is_passive_only(): line.append(self.STATE_ICONS["passive"]) else: line.append(None) # now the service... if item.get_hash() in self.events_history and self.events_history[item.get_hash()] == True: line.append(self.STATE_ICONS["fresh"]) else: line.append(None) if item.is_acknowledged(): line.append(self.STATE_ICONS["acknowledged"]) else: line.append(None) if item.is_in_scheduled_downtime(): line.append(self.STATE_ICONS["downtime"]) else: line.append(None) if item.is_flapping(): line.append(self.STATE_ICONS["flapping"]) else: line.append(None) if item.is_passive_only(): line.append(self.STATE_ICONS["passive"]) else: line.append(None) server.ListStore.append(line) del item, line # give new ListStore to the view, overwrites the old one automatically - theoretically server.TreeView.set_model(server.ListStore) # restore sorting order from previous refresh self.set_sorting(server.ListStore, server) # status field in server vbox in popwin self.popwin.UpdateStatus(server) except: import traceback traceback.print_exc(file=sys.stdout) server.Error(sys.exc_info()) if str(self.conf.fullscreen) == "False": self.popwin.Resize() else: # set every active vbox to window with to avoid too small ugly treeviewa for server in self.servers.values(): if str(self.conf.servers[server.get_name()].enabled) == "True": self.popwin.ServerVBoxes[server.get_name()].set_size_request(self.popwin.Window.get_size()[1], -1) pass # everything OK if unknowns == 0 and warnings == 0 and criticals == 0 and unreachables == 0 and downs == 0 and self.status_ok is not False: self.statusbar.statusbar_labeltext = ' OK ' % (str(self.fontsize), str(self.conf.color_ok_background), str(self.conf.color_ok_text)) self.statusbar.statusbar_labeltext_inverted = self.statusbar.statusbar_labeltext self.statusbar.Label.set_markup(self.statusbar.statusbar_labeltext) # fix size when loading with network errors self.statusbar.Resize() # if all is OK there is no need to pop up popwin so set self.showPopwin to False self.popwin.showPopwin = False self.popwin.PopDown() self.status_ok = True # set systray icon to green aka OK if str(self.conf.icon_in_systray) == "True": self.statusbar.SysTray.set_from_pixbuf(self.statusbar.SYSTRAY_ICONS["green"]) if str(self.conf.appindicator) == "True" and sys.modules.has_key("appindicator"): # greenify status icon self.appindicator.Indicator.set_attention_icon(self.Resources + os.sep + "nagstamon_green" + self.BitmapSuffix) self.appindicator.Indicator.set_status(appindicator.STATUS_ATTENTION) # disable all unneeded menu entries self.appindicator.Menu_OK.hide() self.appindicator.Menu_WARNING.hide() self.appindicator.Menu_UNKNOWN.hide() self.appindicator.Menu_CRITICAL.hide() self.appindicator.Menu_UNREACHABLE.hide() self.appindicator.Menu_DOWN.hide() # switch notification off self.NotificationOff() else: self.status_ok = False # put text for label together self.statusbar.statusbar_labeltext = self.statusbar.statusbar_labeltext_inverted = "" if downs > 0: if str(self.conf.long_display) == "True": downs = str(downs) + " DOWN" self.statusbar.statusbar_labeltext += ' %s ' % (str(self.fontsize), str(self.conf.color_down_background), str(self.conf.color_down_text), str(downs)) self.statusbar.statusbar_labeltext_inverted += ' %s ' % (str(self.fontsize), str(self.conf.color_down_text), str(self.conf.color_down_background), str(downs)) if unreachables > 0: if str(self.conf.long_display) == "True": unreachables = str(unreachables) + " UNREACHABLE" self.statusbar.statusbar_labeltext += ' %s ' % (str(self.fontsize), str(self.conf.color_unreachable_background), str(self.conf.color_unreachable_text), str(unreachables)) self.statusbar.statusbar_labeltext_inverted += ' %s ' % (str(self.fontsize), str(self.conf.color_unreachable_text), str(self.conf.color_unreachable_background), str(unreachables)) if criticals > 0: if str(self.conf.long_display) == "True": criticals = str(criticals) + " CRITICAL" self.statusbar.statusbar_labeltext += ' %s ' % (str(self.fontsize), str(self.conf.color_critical_background), str(self.conf.color_critical_text), str(criticals)) self.statusbar.statusbar_labeltext_inverted += ' %s ' % (str(self.fontsize), str(self.conf.color_critical_text), str(self.conf.color_critical_background), str(criticals)) if unknowns > 0: if str(self.conf.long_display) == "True": unknowns = str(unknowns) + " UNKNOWN" self.statusbar.statusbar_labeltext += ' %s ' % (str(self.fontsize), str(self.conf.color_unknown_background), str(self.conf.color_unknown_text), str(unknowns)) self.statusbar.statusbar_labeltext_inverted += ' %s ' % (str(self.fontsize), str(self.conf.color_unknown_text), str(self.conf.color_unknown_background), str(unknowns)) if warnings > 0: if str(self.conf.long_display) == "True": warnings = str(warnings) + " WARNING" self.statusbar.statusbar_labeltext += ' %s ' % (str(self.fontsize), str(self.conf.color_warning_background), str(self.conf.color_warning_text), str(warnings)) self.statusbar.statusbar_labeltext_inverted += ' %s ' % (str(self.fontsize), str(self.conf.color_warning_text), str(self.conf.color_warning_background), str(warnings)) # if connections fails at starting do not display OK - Debian bug #617490 if unknowns == 0 and warnings == 0 and criticals == 0 and unreachables == 0 and downs == 0 and self.status_ok is False: if str(self.conf.long_display) == "True": errors = "ERROR" else: errors = "ERR" self.statusbar.statusbar_labeltext += ' %s ' % (str(self.fontsize), str(self.conf.color_error_background), str(self.conf.color_error_text), str(errors)) self.statusbar.statusbar_labeltext_inverted += ' %s ' % (str(self.fontsize), str(self.conf.color_error_text), str(self.conf.color_error_background), str(errors)) color = "error" if str(self.conf.appindicator) == "True" and sys.modules.has_key("appindicator"): # set new icon self.appindicator.Indicator.set_attention_icon(self.Resources + os.sep + "nagstamon_error" + self.BitmapSuffix) self.appindicator.Indicator.set_status(appindicator.STATUS_ATTENTION) # put text into label in statusbar, only if not already flashing if self.statusbar.Flashing == False: self.statusbar.Label.set_markup(self.statusbar.statusbar_labeltext) # Windows workaround for non-automatically-shrinking desktop statusbar if str(self.conf.statusbar_floating) == "True": self.statusbar.Resize() # choose icon for systray - the worst case decides the shown color if warnings > 0: color = "yellow" if unknowns > 0: color = "orange" if criticals > 0: color = "red" if unreachables > 0: color = "darkred" if downs > 0: color = "black" if str(self.conf.icon_in_systray) == "True": self.statusbar.SysTray.set_from_pixbuf(self.statusbar.SYSTRAY_ICONS[color]) if str(self.conf.appindicator) == "True" and sys.modules.has_key("appindicator"): # enable/disable menu entries depending on existence of problems if warnings > 0: self.appindicator.Menu_WARNING.set_label(str(warnings) + " WARNING") self.appindicator.Menu_WARNING.show() else: self.appindicator.Menu_WARNING.hide() if unknowns > 0: self.appindicator.Menu_UNKNOWN.set_label(str(unknowns) + " UNKNOWN") self.appindicator.Menu_UNKNOWN.show() else: self.appindicator.Menu_UNKNOWN.hide() if criticals > 0: self.appindicator.Menu_CRITICAL.set_label(str(criticals) + " CRITICAL") self.appindicator.Menu_CRITICAL.show() else: self.appindicator.Menu_CRITICAL.hide() if unreachables > 0: self.appindicator.Menu_UNREACHABLE.set_label(str(unreachables) + " UNREACHABLE") self.appindicator.Menu_UNREACHABLE.show() else: self.appindicator.Menu_UNREACHABLE.hide() if downs > 0: self.appindicator.Menu_DOWN.set_label(str(downs) + " DOWNS") self.appindicator.Menu_DOWN.show() else: self.appindicator.Menu_DOWN.hide() # show nenu entry to acknowledge the notification self.appindicator.Menu_OK.show() # set new icon self.appindicator.Indicator.set_attention_icon(self.Resources + os.sep + "nagstamon_" + color + self.BitmapSuffix) self.appindicator.Indicator.set_status(appindicator.STATUS_ATTENTION) # if there has been any status change notify user # first find out which of all servers states is the worst worst = 0 worst_status = "UP" """ the last worse state should be saved here and taken for comparison to decide if keep warning if the respective (and not yet existing) option is set """ for server in self.servers.values(): if not server.WorstStatus == "UP": # switch server status back because it has been recognized if server.States.index(server.WorstStatus) > worst: worst_status = server.WorstStatus # reset status of the server for only processing it once server.WorstStatus = "UP" if not worst_status == "UP" and str(self.conf.notification) == "True": self.NotificationOn(status=worst_status, ducuw=(downs, unreachables, criticals, unknowns, warnings)) # store latst worst status for decide if there has to be notification action # when all is OK some lines later self.last_worst_status = worst_status # set self.showPopwin to True because there is something to show self.popwin.showPopwin = True # id all gets OK and an notifikation actions is defined run it if self.status_ok and self.last_worst_status != "UP": if str(self.conf.notification_action_ok) == "True": Actions.RunNotificationAction(str(self.conf.notification_action_ok_string)) self.last_worst_status = "UP" # if failures have gone and nobody took notice switch notification off again if len([k for k,v in self.events_history.items() if v == True]) == 0 and self.Notifying == True: self.NotificationOff() # if only one monitor cannot be reached show popwin to inform about its trouble for server in self.servers.values(): if server.status_description != "" or server.refresh_authentication == True: self.status_ok = False self.popwin.showPopwin = True # close popwin in case everything is ok and green if self.status_ok and not self.popwin.showPopwin: self.popwin.Close() # try to fix vanishing statusbar if str(self.conf.statusbar_floating) == "True": self.statusbar.Raise() # return False to get removed as gobject idle source return False def AcknowledgeDialogShow(self, server, host, service=None): """ create and show acknowledge_dialog from gtkbuilder file """ # set the gtkbuilder file self.builderfile = self.Resources + os.sep + "acknowledge_dialog.ui" self.acknowledge_xml = gtk.Builder() self.acknowledge_xml.add_from_file(self.builderfile) self.acknowledge_dialog = self.acknowledge_xml.get_object("acknowledge_dialog") # do not lose dialog behind other windows self.acknowledge_dialog.set_keep_above(True) # connect with action # only OK needs to be connected - if this action gets canceled nothing happens # use connect_signals to assign methods to handlers handlers_dict = { "button_ok_clicked": self.Acknowledge, "button_acknowledge_settings_clicked": self.AcknowledgeDefaultSettings } self.acknowledge_xml.connect_signals(handlers_dict, server) # did not get it to work with glade so comments will be fired up this way when pressing return self.acknowledge_xml.get_object("input_entry_author").connect("key-release-event", self._FocusJump, self.acknowledge_xml, "input_entry_comment") self.acknowledge_xml.get_object("input_entry_comment").connect("key-release-event", self.AcknowledgeCommentReturn, server) # if service is "" it must be a host if service == "": # set label for acknowledging a host self.acknowledge_dialog.set_title("Acknowledge host") self.acknowledge_xml.get_object("input_label_description").set_markup("Host %s" % (host)) else: # set label for acknowledging a service on host self.acknowledge_dialog.set_title("Acknowledge service") self.acknowledge_xml.get_object("input_label_description").set_markup("Service %s on host %s" % (service, host)) # host and service labels are hidden to transport info to OK method self.acknowledge_xml.get_object("input_label_host").set_text(host) self.acknowledge_xml.get_object("input_label_host").hide() self.acknowledge_xml.get_object("input_label_service").set_text(service) self.acknowledge_xml.get_object("input_label_service").hide() # default flags of Nagios acknowledgement self.acknowledge_xml.get_object("input_checkbutton_sticky_acknowledgement").set_active(eval(str(self.conf.defaults_acknowledge_sticky))) self.acknowledge_xml.get_object("input_checkbutton_send_notification").set_active(eval(str(self.conf.defaults_acknowledge_send_notification))) self.acknowledge_xml.get_object("input_checkbutton_persistent_comment").set_active(eval(str(self.conf.defaults_acknowledge_persistent_comment))) self.acknowledge_xml.get_object("input_checkbutton_acknowledge_all_services").set_active(eval(str(self.conf.defaults_acknowledge_all_services))) # default author + comment self.acknowledge_xml.get_object("input_entry_author").set_text(server.username) self.acknowledge_xml.get_object("input_entry_comment").set_text(self.conf.defaults_acknowledge_comment) self.acknowledge_xml.get_object("input_entry_comment").grab_focus() # show dialog self.acknowledge_dialog.run() self.acknowledge_dialog.destroy() def AcknowledgeDefaultSettings(self, foo, bar): """ show settings with tab "defaults" as shortcut from Acknowledge dialog """ self.acknowledge_dialog.destroy() self.GetDialog(dialog="Settings", servers=self.servers, output=self, conf=self.conf, first_page="Defaults") def AcknowledgeCommentReturn(self, widget, event, server): """ if Return key has been pressed in comment entry field interprete this as OK button being pressed """ # KP_Enter seems to be the code for return key of numeric key block if gtk.gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: self.Acknowledge(server=server) self.acknowledge_dialog.destroy() def Acknowledge(self, widget=None, server=None): """ acknowledge miserable host/service """ # various parameters for the CGI request host = self.acknowledge_xml.get_object("input_label_host").get_text() service = self.acknowledge_xml.get_object("input_label_service").get_text() author = self.acknowledge_xml.get_object("input_entry_author").get_text() comment = self.acknowledge_xml.get_object("input_entry_comment").get_text() acknowledge_all_services = self.acknowledge_xml.get_object("input_checkbutton_acknowledge_all_services").get_active() sticky = self.acknowledge_xml.get_object("input_checkbutton_sticky_acknowledgement").get_active() notify = self.acknowledge_xml.get_object("input_checkbutton_send_notification").get_active() persistent = self.acknowledge_xml.get_object("input_checkbutton_persistent_comment").get_active() # create a list of all service of selected host to acknowledge them all all_services = list() if acknowledge_all_services == True: for i in server.nagitems_filtered["services"].values(): for s in i: if s.host == host: all_services.append(s.name) # let thread execute POST request acknowledge = Actions.Acknowledge(server=server, host=host,\ service=service, author=author, comment=comment, acknowledge_all_services=acknowledge_all_services,\ all_services=all_services, sticky=sticky, notify=notify, persistent=persistent) acknowledge.start() def DowntimeDialogShow(self, server, host, service=None): """ create and show downtime_dialog from gtkbuilder file """ # set the gtkbuilder file self.builderfile = self.Resources + os.sep + "downtime_dialog.ui" self.downtime_xml = gtk.Builder() self.downtime_xml.add_from_file(self.builderfile) self.downtime_dialog = self.downtime_xml.get_object("downtime_dialog") # do not lose dialog behind other windows self.downtime_dialog.set_keep_above(True) # connect with action # only OK needs to be connected - if this action gets canceled nothing happens # use connect_signals to assign methods to handlers handlers_dict = { "button_ok_clicked" : self.Downtime, "button_downtime_settings_clicked" : self.DowntimeDefaultSettings } self.downtime_xml.connect_signals(handlers_dict, server) # focus jump chain - used to connect input fields in downtime dialog and access them via return key chain = ["input_entry_start_time", "input_entry_end_time", "input_entry_author", "input_entry_comment"] for i in range(len(chain)-1): self.downtime_xml.get_object(chain[i]).connect("key-release-event", self._FocusJump, self.downtime_xml, chain[i+1]) # if return key enterd in comment field see this as OK button pressed self.downtime_xml.get_object("input_entry_comment").connect("key-release-event", self.DowntimeCommentReturn, server) # if service is None it must be a host if service == "": # set label for acknowledging a host self.downtime_dialog.set_title("Downtime for host") self.downtime_xml.get_object("input_label_description").set_markup("Host %s" % (host)) else: # set label for acknowledging a service on host self.downtime_dialog.set_title("Downtime for service") self.downtime_xml.get_object("input_label_description").set_markup("Service %s on host %s" % (service, host)) # host and service labels are hidden to transport info to OK method self.downtime_xml.get_object("input_label_host").set_text(host) self.downtime_xml.get_object("input_label_host").hide() self.downtime_xml.get_object("input_label_service").set_text(service) self.downtime_xml.get_object("input_label_service").hide() # get start_time and end_time externally from Actions.Downtime_get_start_end() for not mixing GUI and actions too much start_time, end_time = Actions.Downtime_get_start_end(server=server, host=host) self.downtime_xml.get_object("input_radiobutton_type_fixed").set_active(eval(str(self.conf.defaults_downtime_type_fixed))) self.downtime_xml.get_object("input_radiobutton_type_flexible").set_active(eval(str(self.conf.defaults_downtime_type_flexible))) # default author + comment self.downtime_xml.get_object("input_entry_author").set_text(server.username) self.downtime_xml.get_object("input_entry_comment").set_text(self.conf.defaults_downtime_comment) self.downtime_xml.get_object("input_entry_comment").grab_focus() # start and end time self.downtime_xml.get_object("input_entry_start_time").set_text(start_time) self.downtime_xml.get_object("input_entry_end_time").set_text(end_time) # flexible downtime duration self.downtime_xml.get_object("input_spinbutton_duration_hours").set_value(int(self.conf.defaults_downtime_duration_hours)) self.downtime_xml.get_object("input_spinbutton_duration_minutes").set_value(int(self.conf.defaults_downtime_duration_minutes)) # show dialog self.downtime_dialog.run() self.downtime_dialog.destroy() def DowntimeDefaultSettings(self, foo, bar): """ show settings with tab "defaults" as shortcut from Downtime dialog """ self.downtime_dialog.destroy() self.GetDialog(dialog="Settings", servers=self.servers, output=self, conf=self.conf, first_page="Defaults") def DowntimeCommentReturn(self, widget, event, server): """ if Return key has been pressed in comment entry field interprete this as OK button being pressed """ # KP_Enter seems to be the code for return key of numeric key block if gtk.gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: self.Downtime(server=server) self.downtime_dialog.destroy() def Downtime(self, widget=None, server=None): """ schedule downtime for miserable host/service """ # various parameters for the CGI request host = self.downtime_xml.get_object("input_label_host").get_text() service = self.downtime_xml.get_object("input_label_service").get_text() author = self.downtime_xml.get_object("input_entry_author").get_text() comment = self.downtime_xml.get_object("input_entry_comment").get_text() # start and end time start_time = self.downtime_xml.get_object("input_entry_start_time").get_text() end_time = self.downtime_xml.get_object("input_entry_end_time").get_text() # type of downtime - fixed or flexible if self.downtime_xml.get_object("input_radiobutton_type_fixed").get_active() == True: fixed = 1 else: fixed = 0 # duration of downtime if flexible hours = self.downtime_xml.get_object("input_spinbutton_duration_hours").get_value() minutes = self.downtime_xml.get_object("input_spinbutton_duration_minutes").get_value() # execute POST request with cgi_data, in this case threaded downtime = Actions.Downtime(server=server, host=host, service=service, author=author, comment=comment, fixed=fixed, start_time=start_time, end_time=end_time, hours=int(hours), minutes=int(minutes)) downtime.start() def SubmitCheckResultDialogShow(self, server, host, service=None): """ create and show acknowledge_dialog from gtkbuilder file """ # set the gtkbuilder file self.builderfile = self.Resources + os.sep + "submit_check_result_dialog.ui" self.submitcheckresult_xml = gtk.Builder() self.submitcheckresult_xml.add_from_file(self.builderfile) self.submitcheckresult_dialog = self.submitcheckresult_xml.get_object("submit_check_result_dialog") # do not lose dialog behind other windows self.submitcheckresult_dialog.set_keep_above(True) # connect with action # only OK needs to be connected - if this action gets canceled nothing happens # use connect_signals to assign methods to handlers handlers_dict = { "button_ok_clicked" : self.SubmitCheckResultOK,\ "button_cancel_clicked": self.SubmitCheckResultCancel,\ "button_submit_check_result_settings_clicked" : self.SubmitCheckResultDefaultSettings} self.submitcheckresult_xml.connect_signals(handlers_dict, server) # focus jump chain - used to connect input fields in submit check result dialog and access them via return key # server.SUBMIT_CHECK_RESULT_ARGS contains the valid arguments for this server type so we might use it here too chain = server.SUBMIT_CHECK_RESULT_ARGS for i in range(len(chain)-1): self.submitcheckresult_xml.get_object("input_entry_" + chain[i]).connect("key-release-event", self._FocusJump, self.submitcheckresult_xml, "input_entry_" + chain[i+1]) # if return key entered in lastfield see this as OK button pressed self.submitcheckresult_xml.get_object("input_entry_" + chain[-1]).connect("key-release-event", self.SubmitCheckResultCommentReturn, server) # if service is "" it must be a host if service == "": # set label for submitting results to an host self.submitcheckresult_dialog.set_title("Submit check result for host") self.submitcheckresult_xml.get_object("input_label_description").set_markup("Host %s" % (host)) self.submitcheckresult_xml.get_object("input_radiobutton_result_ok").hide() self.submitcheckresult_xml.get_object("input_radiobutton_result_warning").hide() self.submitcheckresult_xml.get_object("input_radiobutton_result_critical").hide() self.submitcheckresult_xml.get_object("input_radiobutton_result_unknown").hide() self.submitcheckresult_xml.get_object("input_radiobutton_result_up").set_active(True) else: # set label for submitting results to a service on host self.submitcheckresult_dialog.set_title("Submit check result for service") self.submitcheckresult_xml.get_object("input_label_description").set_markup("Service %s on host %s" % (service, host)) self.submitcheckresult_xml.get_object("input_radiobutton_result_unreachable").hide() self.submitcheckresult_xml.get_object("input_radiobutton_result_up").hide() self.submitcheckresult_xml.get_object("input_radiobutton_result_down").hide() for i in server.SUBMIT_CHECK_RESULT_ARGS: self.submitcheckresult_xml.get_object("label_" + i).show() self.submitcheckresult_xml.get_object("input_entry_" + i).show() # host and service labels are hidden to transport info to OK method self.submitcheckresult_xml.get_object("input_label_host").set_text(host) self.submitcheckresult_xml.get_object("input_label_host").hide() self.submitcheckresult_xml.get_object("input_label_service").set_text(service) self.submitcheckresult_xml.get_object("input_label_service").hide() self.submitcheckresult_xml.get_object("input_entry_comment").set_text(self.conf.defaults_submit_check_result_comment) # show dialog self.submitcheckresult_dialog.run() def SubmitCheckResultDefaultSettings(self, foo, bar): """ show settings with tab "defaults" as shortcut from Submit Check Result dialog """ self.submitcheckresult_dialog.destroy() self.GetDialog(dialog="Settings", servers=self.servers, output=self, conf=self.conf, first_page="Defaults") def SubmitCheckResultOK(self, widget=None, server=None): """ submit check result """ # various parameters for the CGI request host = self.submitcheckresult_xml.get_object("input_label_host").get_text() service = self.submitcheckresult_xml.get_object("input_label_service").get_text() comment = self.submitcheckresult_xml.get_object("input_entry_comment").get_text() check_output = self.submitcheckresult_xml.get_object("input_entry_check_output").get_text() performance_data = self.submitcheckresult_xml.get_object("input_entry_performance_data").get_text() # dummy default state state = "ok" for s in ["ok", "up", "warning", "critical", "unreachable", "unknown", "down"]: if self.submitcheckresult_xml.get_object("input_radiobutton_result_" + s).get_active() == True: state = s break if "check_output" in server.SUBMIT_CHECK_RESULT_ARGS and len(check_output) == 0: self.Dialog(message="Submit check result needs a check output.") else: # let thread execute POST request submit_check_result = Actions.SubmitCheckResult(server=server, host=host,\ service=service, comment=comment, check_output=check_output,\ performance_data=performance_data, state=state) submit_check_result.start() # only close dialog if input was correct self.submitcheckresult_dialog.destroy() def SubmitCheckResultCancel(self, widget, server): self.submitcheckresult_dialog.destroy() def SubmitCheckResultCommentReturn(self, widget, event, server): """ if Return key has been pressed in comment entry field interprete this as OK button being pressed """ # KP_Enter seems to be the code for return key of numeric key block if gtk.gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: self.SubmitCheckResultOK(server=server) self.submitcheckresult_dialog.destroy() def AboutDialog(self): """ about nagstamon """ about = gtk.AboutDialog() about.set_keep_above(True) about.set_name(self.name) about.set_version(self.version) about.set_website(self.website) about.set_copyright(self.copyright) about.set_comments(self.comments) about.set_authors(["Henri Wahl", " ", "Thank you very much for code", "contributions, patches, packaging,", "testing, hints and ideas:", " ", "Andreas Ericsson", "Antoine Jacoutot", "Anton Löfgren", "Arnaud Gomes", "Benoît Soenen", "Carl Chenet", "Carl Helmertz", "Davide Cecchetto", "Emile Heitor ", "John Conroy", "Lars Michelsen", "M. Cigdem Cebe", "Martin Campbell", "Mattias Ryrlén", "Michał Rzeszut", "Nikita Klimov", "Patrick Cernko", "Pawel Połewicz", "Robin Sonefors", "Salvatore LaMendola", "Sandro Tosi", "Sven Nierlein", "Thomas Gelf", "Tobias Scheerbaum", "Wouter Schoot", "Yannick Charton", " ", "...and those I forgot to mention but who helped a lot...", " ", "Third party software used by Nagstamon", "under their respective license:", "BeautifulSoup - http://www.crummy.com/software/BeautifulSoup", "Pyinstaller - http://www.pyinstaller.org"]) # read LICENSE file license = "" try: # try to find license file in resource directory f = open(self.Resources + os.sep + "LICENSE", "r") s = f.readlines() f.close() for line in s: license += line except: license = "Nagstamon is licensed under GPL 2.0.\nYou should have got a LICENSE file distributed with nagstamon.\nGet it at http://www.gnu.org/licenses/gpl-2.0.txt." about.set_license(license) # use gobject.idle_add() to be thread safe gobject.idle_add(self.AddGUILock, str(self.__class__.__name__)) self.popwin.Close() about.run() # use gobject.idle_add() to be thread safe gobject.idle_add(self.DeleteGUILock, str(self.__class__.__name__)) about.destroy() def Dialog(self, type=gtk.MESSAGE_ERROR, message="", buttons=gtk.BUTTONS_CANCEL): """ versatile message dialog """ # close popwin to make sure the error dialog will not be covered by popwin self.popwin.PopDown() self.dialog = gtk.MessageDialog(parent=None, flags=gtk.DIALOG_MODAL, type=type, buttons=buttons, message_format=str(message)) # gtk.Dialog.run() does a mini loop to wait self.dialog.run() self.dialog.destroy() def CheckForNewVersionDialog(self, version_status=None, version=None): """ Show results of Settings.CheckForNewVersion() """ try: # close popwin to make sure the error dialog will not be covered by popwin self.popwin.PopDown() # if used version is latest version only inform about if version_status == "latest": self.dialog = gtk.MessageDialog(parent=None, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK, \ message_format="You are already using the\nlatest version of Nagstamon.\n\nLatest version: %s" % (version)) # keep message dialog in front self.dialog.set_keep_above(True) self.dialog.present() self.dialog.run() self.dialog.destroy() # if used version is out of date offer downloading latest one elif version_status == "out_of_date": self.dialog = gtk.MessageDialog(parent=None, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_YES_NO, \ message_format="You are not using the latest version of Nagstamon.\n\nYour version:\t\t%s\nLatest version:\t%s\n\nDo you want to download the latest version?" % (self.version, version)) # keep message dialog in front self.dialog.set_keep_above(True) self.dialog.present() response = self.dialog.run() if response == gtk.RESPONSE_YES: Actions.OpenNagstamonDownload(output=self) self.dialog.destroy() except: self.servers.values()[0].Error(sys.exc_info()) # return False to get removed as gobject idle source return False def NotificationOn(self, status="UP", ducuw=None): """ switch on whichever kind of notification ducuw = downs, unreachables, criticals, unknowns, warnings """ try: # check if notification for status is wanted if not status == "UP" and str(self.conf.__dict__["notify_if_" + status.lower()]) == "True": # only notify if popwin not already popped up if (self.popwin.Window.get_properties("visible")[0] == False and\ str(self.conf.fullscreen) == "False")\ or\ (self.popwin.Window.get_properties("visible")[0] == True and\ str(self.conf.fullscreen) == "True"): if self.Notifying == False: self.Notifying = True # debug if str(self.conf.debug_mode) == "True": self.servers.values()[0].Debug(debug="Notification on.") # threaded statusbar flash if str(self.conf.notification_flashing) == "True": if str(self.conf.icon_in_systray) == "True": self.statusbar.SysTray.set_blinking(True) elif str(self.conf.statusbar_floating) == "True": self.statusbar.Flashing = True elif str(self.conf.appindicator) == "True": self.appindicator.Flashing = True # flashing notification notify = Actions.Notification(output=self, sound=status, Resources=self.Resources, conf=self.conf, servers=self.servers) notify.start() # just playing with libnotify if str(self.conf.notification_desktop) == "True": trouble = "" if ducuw[0] > 0 : trouble += ducuw[0] + " " if ducuw[1] > 0 : trouble += ducuw[1] + " " if ducuw[2] > 0 : trouble += ducuw[2] + " " if ducuw[3] > 0 : trouble += ducuw[3] + " " if ducuw[4] > 0 : trouble += ducuw[4] self.notify_bubble = pynotify.Notification ("Nagstamon", trouble, self.Resources + os.sep + "nagstamon" + self.BitmapSuffix) # only offer button for popup window when floating statusbar is used if str(self.conf.statusbar_floating) == "True": self.notify_bubble.add_action("action", "Open popup window", self.popwin.PopUp) self.notify_bubble.show() # Notification actions if str(self.conf.notification_actions) == "True": if str(self.conf.notification_action_warning) == "True" and status == "WARNING": Actions.RunNotificationAction(str(self.conf.notification_action_warning_string)) if str(self.conf.notification_action_critical) == "True" and status == "CRITICAL": Actions.RunNotificationAction(str(self.conf.notification_action_critical_string)) if str(self.conf.notification_action_down) == "True" and status == "DOWN": Actions.RunNotificationAction(str(self.conf.notification_action_down_string)) # if desired pop up status window # sorry but does absolutely not work with windows and systray icon so I prefer to let it be #if str(self.conf.notification_popup) == "True": # self.popwin.showPopwin = True # self.popwin.PopUp() # Custom event notification if str(self.conf.notification_actions) == "True" and str(self.conf.notification_custom_action) == "True": events = "" # if no single notifications should be used (default) put all events into one string, separated by separator if str(self.conf.notification_custom_action_single) == "False": # list comprehension only considers events which are new, ergo True events = self.conf.notification_custom_action_separator.join([k for k,v in self.events_notification.items() if v == True]) # clear already notified events setting them to False for event in [k for k,v in self.events_notification.items() if v == True]: self.events_notification[event] = False else: for event in [k for k,v in self.events_notification.items() if v == True]: custom_action_string = self.conf.notification_custom_action_string.replace("$EVENTS$", event) Actions.RunNotificationAction(custom_action_string) # clear already notified events setting them to False self.events_notification[event] = False # if events got filled display them now if events != "": # in case a single action per event has to be executed custom_action_string = self.conf.notification_custom_action_string.replace("$EVENT$", "$EVENTS$") # insert real event(s) custom_action_string = custom_action_string.replace("$EVENTS$", events) Actions.RunNotificationAction(custom_action_string) else: # set all events to False to ignore them in the future for event in self.events_notification: self.events_notification[event] = False except: self.servers.values()[0].Error(sys.exc_info()) def NotificationOff(self, widget=None): """ switch off whichever kind of notification """ if self.Notifying == True: self.Notifying = False # debug if str(self.conf.debug_mode) == "True": self.servers.values()[0].Debug(debug="Notification off.") if str(self.conf.icon_in_systray) == "True": self.statusbar.SysTray.set_blinking(False) elif str(self.conf.statusbar_floating) == "True": self.statusbar.Flashing = False self.statusbar.Label.set_markup(self.statusbar.statusbar_labeltext) # resize statusbar to avoid artefact when showing error self.statusbar.Resize() elif str(self.conf.appindicator) == "True" and sys.modules.has_key("appindicator"): self.appindicator.Flashing = False self.appindicator.Indicator.set_status(appindicator.STATUS_ATTENTION) def RecheckAll(self, widget=None): """ call threaded recheck all action """ # first delete all freshness flags self.UnfreshEventHistory() # run threads for rechecking recheckall = Actions.RecheckAll(servers=self.servers, output=self, conf=self.conf) recheckall.start() def _GetAlternateColor(self, color, diff=2048): """ helper for treeview table colors to get a slightly different color """ if color > (65535 - diff): color = color - diff else: color = color + diff return color def AddGUILock(self, widget_name, widget=None): """ add calling window to dictionary of open windows to keep the windows separated to be called via gobject.idle_add """ self.GUILock[widget_name] = widget # return False to get removed as gobject idle source return False def DeleteGUILock(self, window_name): """ delete calling window from dictionary of open windows to keep the windows separated to be called via gobject.idle_add """ try: self.GUILock.pop(window_name) except: #import traceback #traceback.print_exc(file=sys.stdout) pass # return False to get removed as gobject idle source return False def _FocusJump(self, widget=None, event=None, builder=None, next_widget=None): """ if Return key has been pressed in entry field jump to given widget """ if gtk.gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: builder.get_object(next_widget).grab_focus() def Exit(self, dummy): """ exit.... """ self.conf.SaveConfig(output=self) gtk.main_quit() def GetDialog(self, **kwds): """ Manage dialogs et al. so they would not have been re-created with every access Hoping to decrease memory usage this way """ for k in kwds: self.__dict__[k] = kwds[k] # create dialogs if not yet existing if not self.dialog in self.Dialogs: if self.dialog == "Settings": gobject.idle_add(self.output.AddGUILock, "Settings") self.Dialogs["Settings"] = Settings(servers=self.servers, output=self.output, conf=self.conf, first_page=self.first_page) self.Dialogs["Settings"].show() elif self.dialog == "NewServer": gobject.idle_add(self.output.AddGUILock, "NewServer") self.Dialogs["NewServer"] = NewServer(servers=self.servers, output=self.output, settingsdialog=self.settingsdialog, conf=self.conf) self.Dialogs["NewServer"].show() elif self.dialog == "EditServer": gobject.idle_add(self.output.AddGUILock, "EditServer") self.Dialogs["EditServer"] = EditServer(servers=self.servers, output=self.output, settingsdialog=self.settingsdialog, conf=self.conf, server=self.selected_server) self.Dialogs["EditServer"].show() elif self.dialog == "CopyServer": gobject.idle_add(self.output.AddGUILock, "CopyServer") self.Dialogs["CopyServer"] = CopyServer(servers=self.servers, output=self.output, settingsdialog=self.settingsdialog, conf=self.conf, server=self.selected_server) self.Dialogs["CopyServer"].show() elif self.dialog == "NewAction": gobject.idle_add(self.output.AddGUILock, "NewAction") self.Dialogs["NewAction"] = NewAction(output=self.output, settingsdialog=self.settingsdialog, conf=self.conf) self.Dialogs["NewAction"].show() elif self.dialog == "EditAction": gobject.idle_add(self.output.AddGUILock, "EditAction") self.Dialogs["EditAction"] = EditAction(output=self.output, settingsdialog=self.settingsdialog, conf=self.conf, action=self.selected_action) self.Dialogs["EditAction"].show() elif self.dialog == "CopyAction": gobject.idle_add(self.output.AddGUILock, "CopyAction") self.Dialogs["CopyAction"] = CopyAction(output=self.output, settingsdialog=self.settingsdialog, conf=self.conf, action=self.selected_action) self.Dialogs["CopyAction"].show() else: # when being reused some dialogs need some extra values if self.dialog in ["Settings", "NewServer", "EditServer", "CopyServer", "NewAction", "EditAction", "CopyAction"]: self.output.popwin.Close() gobject.idle_add(self.output.AddGUILock, self.dialog) if self.dialog == "Settings": self.Dialogs["Settings"].first_page = self.first_page if self.dialog == "EditServer": self.Dialogs["EditServer"].server = self.selected_server if self.dialog == "CopyServer": self.Dialogs["CopyServer"].server = self.selected_server if self.dialog == "EditAction": self.Dialogs["EditAction"].action = self.selected_action if self.dialog == "CopyAction": self.Dialogs["CopyAction"].action = self.selected_action self.Dialogs[self.dialog].initialize() self.Dialogs[self.dialog].show() def UnfreshEventHistory(self): # set all flagged-as-fresh-events to un-fresh for event in self.events_history.keys(): self.events_history[event] = False def ApplyServerModifications(self): """ used by every dialog that modifies server settings """ # kick out deleted or renamed servers, # create new ones for new, renamed or re-enabled ones for server in self.servers.values(): if not server.get_name() in self.popwin.ServerVBoxes: self.popwin.ServerVBoxes[server.get_name()] = self.popwin.CreateServerVBox(server.get_name(), self) if str(self.conf.servers[server.get_name()].enabled)== "True": self.popwin.ServerVBoxes[server.get_name()].set_visible(True) self.popwin.ServerVBoxes[server.get_name()].set_no_show_all(False) self.popwin.ServerVBoxes[server.get_name()].show_all() #self.output.popwin.ServerVBoxes[server.get_name()].Label.set_markup('%s@%s' % (server.get_username(), server.get_name())) # refresh servervboxes self.popwin.ServerVBoxes[server.get_name()].initialize(server) # add box to the other ones self.popwin.ScrolledVBox.add(self.popwin.ServerVBoxes[server.get_name()]) # add server sorting self.last_sorting[server.get_name()] = Sorting([(self.startup_sort_field,\ self.startup_sort_order ),\ (server.HOST_COLUMN_ID, gtk.SORT_ASCENDING)],\ len(server.COLUMNS)+1) # delete not-current-anymore servers (disabled or renamed) for server in self.popwin.ServerVBoxes.keys(): if not server in self.servers: self.popwin.ServerVBoxes[server].hide_all() self.popwin.ServerVBoxes[server].destroy() self.popwin.ServerVBoxes.pop(server) # reorder server VBoxes in case some names changed # to sort the monitor servers alphabetically make a sortable list of their names server_list = [] for server in self.conf.servers: if str(self.conf.servers[server].enabled) == "True": server_list.append(server) else: # destroy disabled server vboxes if they exist if server in self.popwin.ServerVBoxes: self.popwin.ServerVBoxes[server].destroy() self.popwin.ServerVBoxes.pop(server) server_list.sort(key=str.lower) # sort server vboxes for server in server_list: # refresh servervboxes self.popwin.ServerVBoxes[server].initialize(self.servers[server]) self.popwin.ScrolledVBox.reorder_child(self.popwin.ServerVBoxes[server], server_list.index(server)) # refresh servers combobox in popwin # first remove all entries for i in range(1, len(self.popwin.ComboboxMonitor.get_model())): # "Go to monitor..." is the first entry so do not delete item index 0 self.popwin.ComboboxMonitor.remove_text(1) # sort server names for list server_list = list() for server in self.conf.servers.keys(): server_list.append(server) server_list.sort(key=str.lower) # add all servers in sorted order for server in server_list: self.popwin.ComboboxMonitor.append_text(server) # brutal renewal of popup menu for of statusbar because servers might have been added self.output.statusbar.Menu.destroy() self.output.statusbar._CreateMenu() if self.conf.appindicator == True: # otherwise Ubuntu loses its Nagstamon submenu self.output.appindicator.Menu_Nagstamon.set_submenu(self.output.statusbar.Menu) # force refresh Actions.RefreshAllServers(servers=self.servers, output=self, conf=self.conf) class StatusBar(object): """ statusbar object with appended systray icon """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] self._CreateFloatingStatusbar() # image for logo in statusbar self.nagstamonLogo = gtk.Image() self.nagstamonLogo.set_from_file(self.output.Resources + os.sep + "nagstamon_small" + self.output.BitmapSuffix) # icons for systray self.SYSTRAY_ICONS = dict() for color in ["green", "yellow", "red", "darkred", "orange", "black","error"]: self.SYSTRAY_ICONS[color] = gtk.gdk.pixbuf_new_from_file(self.output.Resources + os.sep + "nagstamon_" + color + self.output.BitmapSuffix) # 2 versions of label text for notification self.statusbar_labeltext = "" self.statusbar_labeltext_inverted = "" self.Flashing = False # Label for display self.Label = gtk.Label() # statusbar hbox container for logo and status label self.HBox = gtk.HBox() # Use EventBox because Label cannot get events self.LogoEventbox = gtk.EventBox() self.LogoEventbox.add(self.nagstamonLogo) self.EventBoxLabel = gtk.EventBox() self.EventBoxLabel.add(self.Label) self.HBox.add(self.LogoEventbox) self.HBox.add(self.EventBoxLabel) self.StatusBar.add(self.HBox) # trying a workaround for windows gtk 2.22 not letting statusbar being dragged around self.Moving = False # if statusbar is enabled... self.StatusBar.move(int(self.conf.position_x), int(self.conf.position_y)) if str(self.conf.statusbar_floating) == "True": # ...move statusbar in case it is floating to its last saved position and show it self.StatusBar.show_all() else: self.StatusBar.hide_all() # put Systray icon into statusbar object # on MacOSX use only dummy if platform.system() == "Darwin": self.SysTray = DummyStatusIcon() else: self.SysTray = gtk.StatusIcon() self.SysTray.set_from_file(self.output.Resources + os.sep + "nagstamon" + self.output.BitmapSuffix) # if systray icon should be shown show it if str(self.conf.icon_in_systray) == "False": self.SysTray.set_visible(False) else: self.SysTray.set_visible(True) # flag to lock statusbar error messages not to provoke a pango crash self.isShowingError = False self.CalculateFontSize() # Popup menu for statusbar self._CreateMenu() def _CreateMenu(self): """ create statusbar menu, to be used by statusbar initialization and after Settings changes """ # Popup menu for statusbar self.Menu = gtk.Menu() for i in ["Refresh", "Recheck all", "-----", "Monitors", "-----", "Settings...", "Save position", "About", "Exit"]: if i == "-----": menu_item = gtk.SeparatorMenuItem() self.Menu.append(menu_item) else: if i == "Monitors": monitor_items = list(self.output.servers) monitor_items.sort(key=str.lower) for m in monitor_items: menu_item = gtk.MenuItem(m) menu_item.connect("activate", self.MenuResponseMonitors, m) self.Menu.append(menu_item) else: menu_item = gtk.MenuItem(i) menu_item.connect("activate", self.MenuResponse, i) self.Menu.append(menu_item) self.Menu.show_all() def CalculateFontSize(self): """ adapt label font size to nagstamon logo height because on different platforms default sizes + fonts vary """ try: fontsize = 7000 self.Label.set_markup(' Loading... ' % (fontsize)) # compare heights, height of logo is the important one while self.LogoEventbox.size_request()[1] > self.Label.size_request()[1]: self.Label.set_markup(' Loading... ' % (fontsize)) fontsize += 250 # take away some pixels to fit into status bar self.output.fontsize = fontsize - 250 except: # in case of error define fixed fontsize self.output.fontsize = 10000 def _CreateFloatingStatusbar(self): """ create statusbar as floating window """ # TOPLEVEL seems to be more standard compliant if platform.system() == "Windows" or platform.system() == "Darwin": self.StatusBar = gtk.Window(gtk.WINDOW_POPUP) else: self.StatusBar = gtk.Window(gtk.WINDOW_TOPLEVEL) self.StatusBar.set_decorated(False) self.StatusBar.set_keep_above(True) # newer Ubuntus place a resize widget onto floating statusbar - please don't! self.StatusBar.set_resizable(False) self.StatusBar.stick() # at http://www.pygtk.org/docs/pygtk/gdk-constants.html#gdk-window-type-hint-constants # there are some hint types to experiment with # see https://github.com/HenriWahl/Nagstamon/issues/51 if platform.system() == "Windows": self.StatusBar.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) else: self.StatusBar.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_MENU) self.StatusBar.set_property("skip-taskbar-hint", True) self.StatusBar.set_skip_taskbar_hint(True) def MenuPopup(self, widget=None, event=None, time=None, dummy=None): """ context menu for label in statusbar """ self.output.popwin.Close() # no dragging of statusbar anymore if menu pops up self.Moving = False # check if settings are not already open if not "Settings" in self.output.GUILock: # for some reason StatusIcon delivers another event (type int) than # egg.trayicon (type object) so it must be checked which one has # been calling # to make it even worse there are different integer types given back # in Windows and Unix if isinstance(event, int) or isinstance(event, long): # right button if event == 3: # 'time' is important (wherever it comes from) for Linux/Gtk to let # the popup be shown even after releasing the mouse button self.Menu.popup(None, None, None, event, time) else: # right button if event.button == 3: self.Menu.popup(None, None, None, event.button, event.time) # silly Windows(TM) workaround to keep menu above taskbar if not platform.system() == "Darwin": self.Menu.window.set_keep_above(True) def MenuResponseMonitors(self, widget, menu_entry): """ open responding Nagios status web page """ self.output.servers[menu_entry].OpenBrowser(url_type="monitor") def MenuResponse(self, widget, menu_entry): """ responses for the context menu for label in statusbar """ if menu_entry == "Refresh": Actions.RefreshAllServers(servers=self.output.servers, output=self.output, conf=self.conf) if menu_entry == "Recheck all": self.output.RecheckAll() if menu_entry == "Settings...": self.output.GetDialog(dialog="Settings", servers=self.output.servers, output=self.output, conf=self.conf, first_page="Servers") if menu_entry == "Save position": self.conf.SaveConfig(output=self.output) if menu_entry == "About": self.output.AboutDialog() if menu_entry == "Exit": self.conf.SaveConfig(output=self.output) gtk.main_quit() def Clicked(self, widget=None, event=None): """ see what happens if statusbar is clicked """ # check if settings etc. are not already open if self.output.popwin.IsWanted() == True: # if left mousebutton is pressed if event.button == 1: # if popping up on click is true... if str(self.conf.popup_details_clicking) == "True": #... and summary popup not shown yet... if self.output.popwin.Window.get_properties("visible")[0] == False: #...show it... self.output.popwin.PopUp() else: #...otherwise close it self.output.popwin.Close() # if hovering is set, popwin is open and statusbar gets clicked... else: # close popwin for convinience if self.output.popwin.Window.get_properties("visible")[0] == True: self.output.popwin.Close() # if right mousebutton is pressed show statusbar menu if event.button == 3: #self.output.popwin.Close() self.Moving = False self.MenuPopup(widget=self.Menu, event=event) # switch off Notification self.output.NotificationOff() def LogoClicked(self, widget=None, event=None): """ see what happens if statusbar is clicked """ # check if settings etc. are not already open - an open popwin will be closed anyway if len(self.output.GUILock) == 0 or self.output.GUILock.has_key("Popwin"): # get position of statusbar self.StatusBar.x = event.x self.StatusBar.y = event.y # if left mousebutton is pressed if event.button == 1: self.output.popwin.Close() self.Moving = True move = Actions.MoveStatusbar(output=self.output) move.start() # if right mousebutton is pressed show statusbar menu if event.button == 3: self.output.popwin.Close() self.Moving = False self.MenuPopup(widget=self.Menu, event=event) def LogoReleased(self, widget=None, event=None): """ used when button click on logo is released """ self.output.popwin.setShowable() self.Moving = False # to avoid wrong placed popwin in macosx gobject.idle_add(self.output.RefreshDisplayStatus) def SysTrayClicked(self, widget=None, event=None): """ see what happens when icon in systray has been clicked """ # workaround for continuous popup menu try: self.Menu.popdown() except: import traceback traceback.print_exc(file=sys.stdout) # switch notification off self.output.NotificationOff() # check if settings and other dialogs are not already open if self.output.popwin.IsWanted() == True: # if popwin is not shown pop it up if self.output.popwin.Window.get_properties("visible")[0] == False or len(self.output.GUILock) == 0: # workaround for Windows loyal popwin bug # https://github.com/HenriWahl/Nagstamon/issues/63 if self.output.popwin.mousex == self.output.popwin.mousey == 0: rootwin = self.StatusBar.get_screen().get_root_window() self.output.popwin.mousex, self.output.popwin.mousey, foo = rootwin.get_pointer() self.output.popwin.PopUp() else: self.output.popwin.Close() def Hovered(self, widget=None, event=None): """ see what happens if statusbar is hovered """ # check if any dialogs are not already open and pointer does not come # directly from popwin - to avoid flicker and artefact if self.output.popwin.IsWanted() == True and\ str(self.conf.popup_details_hover) == "True" and\ self.output.popwin.pointer_left_popwin == False: self.output.popwin.PopUp() def Move(self, widget=None, event=None): """ moving statusbar """ # access to rootwindow to get the pointers coordinates rootwin = self.StatusBar.get_screen().get_root_window() # get position of the pointer mousex, mousey, foo = rootwin.get_pointer() self.conf.position_x = int(mousex - self.StatusBar.x) self.conf.position_y = int(mousey - self.StatusBar.y) self.StatusBar.move(self.conf.position_x, self.conf.position_y) # return False to get removed as gobject idle source return False def ShowErrorMessage(self, message): """ Shows error message in statusbar """ try: # set flag to locked self.isShowingError = True self.Label.set_markup(' %s ' % (self.output.fontsize, message)) # Windows workaround for non-shrinking desktop statusbar self.Resize() # change systray icon to error self.SysTray.set_from_pixbuf(self.SYSTRAY_ICONS["error"]) # Windows workaround for non-shrinking desktop statusbar self.Resize() except: self.servers.values()[0].Error(sys.exc_info()) # return False to get removed as gobject idle source return False def Flash(self): """ Flashing notification, triggered by threaded RefreshLoop """ # replace statusbar label text with its inverted version if self.Label.get_label() == self.statusbar_labeltext: self.Label.set_markup(self.statusbar_labeltext_inverted) else: self.Label.set_markup(self.statusbar_labeltext) # Windows workaround for non-automatically-shrinking desktop statusbar if str(self.conf.statusbar_floating) == "True": try: self.Resize() except: self.servers.values()[0].Error(sys.exc_info()) # return False to get removed as gobject idle source return False def Resize(self): """ Resize/fix statusbar """ try: x,y = self.Label.size_request() self.StatusBar.resize(x, y) except: self.StatusBar.resize(1, 1) def Raise(self): """ try to fix Debian bug #591875: eventually ends up lower in the window stacking order, and can't be raised raising statusbar window with every refresh should do the job also do NOT raise if statusbar menu is open because otherwise it will be overlapped """ if str(self.conf.statusbar_floating) == "True": # always raise on Windows plus # workaround for statusbar-that-overlaps-popup-menu (oh my god) if platform.system() == "Windows": if not self.Menu.get_properties("visible")[0]: self.StatusBar.window.raise_() # on Linux & Co. only raise if popwin is not shown because otherwise # the statusbar shadow overlays the popwin on newer desktop environments elif self.output.popwin.showPopwin == False: self.StatusBar.window.raise_() class Popwin(object): """ Popwin object """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] # Initialize type popup if platform.system() == "Darwin": self.Window = gtk.Window(gtk.WINDOW_POPUP) else: self.Window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.Window.set_title(self.output.name + " " + self.output.version) # notice leaving cursor self.Window.connect("leave-notify-event", self.LeavePopWin) # initialize the coordinates of left upper corner of the popwin and its size self.popwinx0 = self.popwiny0 = 0 self.popwinwidth = self.popwinheight = 0 self.AlMonitorLabel = gtk.Alignment(xalign=0, yalign=0.5) self.AlMonitorComboBox = gtk.Alignment(xalign=0, yalign=0.5) self.AlMenu = gtk.Alignment(xalign=1.0, yalign=0.5) self.AlVBox = gtk.Alignment(xalign=0, yalign=0, xscale=0, yscale=0) self.VBox = gtk.VBox() self.HBoxAllButtons = gtk.HBox() self.HBoxNagiosButtons = gtk.HBox() self.HBoxMenu = gtk.HBox() self.HBoxCombobox = gtk.HBox() # put a name tag where there buttons had been before # image for logo in statusbar # use pixbuf to keep transparency which itself should keep some padding if popup is oversized self.NagstamonLabel = gtk.Image() self.NagstamonLabel_Pixbuf = gtk.gdk.pixbuf_new_from_file(self.output.Resources + os.sep + "nagstamon_label.png") self.NagstamonLabel.set_from_pixbuf(self.NagstamonLabel_Pixbuf) self.NagstamonVersion = gtk.Label() self.NagstamonVersion.set_markup("%s " % (self.output.version)) self.HBoxNagiosButtons.add(self.NagstamonLabel) self.HBoxNagiosButtons.add(self.NagstamonVersion) self.AlMonitorLabel.add(self.HBoxNagiosButtons) self.ComboboxMonitor = gtk.combo_box_new_text() # fill Nagios server combobox with nagios servers self.ComboboxMonitor.append_text("Go to monitor...") submenu_items = list(self.output.servers) submenu_items.sort(key=str.lower) for i in submenu_items: self.ComboboxMonitor.append_text(i) # set first item active self.ComboboxMonitor.set_active(0) # add conmbobox to right-side menu self.AlMonitorComboBox.add(self.ComboboxMonitor) self.HBoxCombobox.add(self.AlMonitorComboBox) self.HBoxMenu.add(self.HBoxCombobox) # general buttons self.ButtonFilters = ButtonWithIcon(output=self.output, label="Filters", icon="settings.png") self.ButtonRecheckAll = ButtonWithIcon(output=self.output, label="Recheck all", icon="recheckall.png") self.ButtonRefresh = ButtonWithIcon(output=self.output, label="Refresh", icon="refresh.png") self.ButtonSettings = ButtonWithIcon(output=self.output, label="Settings", icon="settings.png") self.HBoxMenu.add(self.ButtonFilters) self.HBoxMenu.add(self.ButtonRecheckAll) self.HBoxMenu.add(self.ButtonRefresh) self.HBoxMenu.add(self.ButtonSettings) # nice separator self.HBoxMenu.add(gtk.VSeparator()) self.ButtonMenu = ButtonWithIcon(output=self.output, label="", icon="menu.png") self.HBoxMenu.add(self.ButtonMenu) self.ButtonMenu.connect("button-press-event", self.MenuPopUp) self.ButtonClose = ButtonWithIcon(output=self.output, label="", icon="close.png") self.HBoxMenu.add(self.ButtonClose) # close popwin when its close button is pressed self.ButtonClose.connect("clicked", self.Close) self.ButtonClose.connect("leave-notify-event", self.LeavePopWin) # for whatever reason in Windows the Filters button grabs initial focus # so the close button should grab it for cosmetical reasons self.ButtonClose.grab_focus() # put the HBox full of buttons full of HBoxes into the aligned HBox... self.AlMenu.add(self.HBoxMenu) # HBoxes en masse... self.HBoxAllButtons.add(self.AlMonitorLabel) #self.HBoxAllButtons.add(self.AlMonitorComboBox) self.HBoxAllButtons.add(self.AlMenu) # threaded recheck all when refresh is clicked self.ButtonRecheckAll.connect("clicked", self.output.RecheckAll) # threaded refresh status information when refresh is clicked self.ButtonRefresh.connect("clicked", lambda r: Actions.RefreshAllServers(servers=self.output.servers, output=self.output, conf=self.conf)) # open settings dialog when settings is clicked self.ButtonSettings.connect("clicked", lambda s: self.output.GetDialog(dialog="Settings", servers=self.output.servers, output=self.output, conf=self.conf, first_page="Servers")) # open settings dialog for filters when filters is clicked self.ButtonFilters.connect("clicked", lambda s: self.output.GetDialog(dialog="Settings", servers=self.output.servers, output=self.output, conf=self.conf, first_page="Filters")) # Workaround for behavorial differences of GTK in Windows and Linux # in Linux it is enough to check for the pointer leaving the whole popwin, # in Windows it is not, here every widget on popwin has to be heard # the intended effect is that popwin closes when the pointer leaves it self.ButtonRefresh.connect("leave-notify-event", self.LeavePopWin) self.ButtonSettings.connect("leave-notify-event", self.LeavePopWin) # define colors for detailed status table in dictionaries # need to be redefined here for MacOSX because there it is not # possible to reinitialize the whole GUI after config changes without a crash self.output.TAB_BG_COLORS = { "UNKNOWN":str(self.conf.color_unknown_background), "CRITICAL":str(self.conf.color_critical_background), "WARNING":str(self.conf.color_warning_background), "DOWN":str(self.conf.color_down_background), "UNREACHABLE":str(self.conf.color_unreachable_background) } self.output.TAB_FG_COLORS = { "UNKNOWN":str(self.conf.color_unknown_text), "CRITICAL":str(self.conf.color_critical_text), "WARNING":str(self.conf.color_warning_text), "DOWN":str(self.conf.color_down_text), "UNREACHABLE":str(self.conf.color_unreachable_text) } # create a scrollable area for the treeview in case it is larger than the screen # in case there are too many failed services and hosts self.ScrolledWindow = gtk.ScrolledWindow() self.ScrolledWindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) # try putting everything status-related into a scrolled viewport self.ScrolledVBox = gtk.VBox() #self.ScrolledWindow.add_with_viewport(self.ScrolledVBox) self.ScrolledViewport = gtk.Viewport() self.ScrolledViewport.add(self.ScrolledVBox) self.ScrolledWindow.add(self.ScrolledViewport) # menu in upper right corner for fullscreen mode self.Menu = gtk.Menu() for i in ["About", "Exit"]: if i == "-----": menu_item = gtk.SeparatorMenuItem() self.Menu.append(menu_item) else: menu_item = gtk.MenuItem(i) menu_item.connect("activate", self.MenuResponse, i) self.Menu.append(menu_item) self.Menu.show_all() # group server infos in VBoxes self.ServerVBoxes = dict() # to sort the Nagios servers alphabetically make a sortable list of their names server_list = list(self.output.servers) server_list.sort(key=str.lower) # create table with all the displayed info for server in server_list: # only if server is enabled if str(self.conf.servers[server].enabled) == "True": self.ServerVBoxes[server] = self.CreateServerVBox(server, self.output) # add box to the other ones self.ScrolledVBox.add(self.ServerVBoxes[server]) # add all buttons in their hbox to the overall vbox self.VBox.add(self.HBoxAllButtons) # put scrolled window aka scrolled treeview into vbox self.VBox.add(self.ScrolledWindow) # put this vbox into popwin self.AlVBox.add(self.VBox) self.Window.add(self.AlVBox) # Initialize show_popwin - show it or not, if everything is OK # it is not necessary to pop it up self.showPopwin = False # measure against artefactional popwin self.pointer_left_popwin = False # flag for deciding if coordinates of statusbar need to be reinvestigated, # only necessary after popping up self.calculate_coordinates = True # add some buffer pixels to popwinheight to avoid silly scrollbars self.heightbuffer_internal = 10 if platform.system() != "Windows" and self.Window.get_screen().get_n_monitors() > 1: self.heightbuffer_external = 30 else: self.heightbuffer_external = 0 # switch between fullscreen and popup mode self.SwitchMode() # helpers for Windows jumping popwin bug # https://github.com/HenriWahl/Nagstamon/issues/63 self.mousex = 0 self.mousey = 0 def SwitchMode(self): """ switch between fullscreen and popup window mode """ try: if str(self.output.conf.fullscreen) == "False": # for not letting statusbar throw a shadow onto popwin in any composition-window-manager this helps to # keep a more consistent look - copied from StatusBar... anyway, doesn't work... well, next attempt: # Windows will have an entry on taskbar when not using HINT_UTILITY ###self.Window.set_visible(False) if platform.system() == "Windows": self.Window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_UTILITY) else: self.Window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_MENU) # make a nice popup of the toplevel window self.Window.set_decorated(False) self.Window.set_keep_above(True) self.Window.unfullscreen() # newer Ubuntus place a resize widget onto floating statusbar - please don't! self.Window.set_resizable(False) self.Window.set_property("skip-taskbar-hint", True) self.Window.stick() self.Window.set_skip_taskbar_hint(True) # change Close/Menu button in popup-mode self.ButtonClose.set_no_show_all(True) self.ButtonClose.show() else: # find out dimension of all monitors if len(self.output.monitors) == 0: for m in range(self.Window.get_screen().get_n_monitors()): monx0, mony0, monw, monh = self.Window.get_screen().get_monitor_geometry(m) self.output.monitors[m] = (monx0, mony0, monw, monh) self.Window.set_visible(False) self.Window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_NORMAL) self.Window.set_visible(True) x0, y0, width, height = self.output.monitors[int(self.output.conf.fullscreen_display)] self.Window.move(x0, y0) self.Window.set_decorated(True) self.Window.set_keep_above(False) self.Window.set_resizable(True) self.Window.set_property("skip-taskbar-hint", False) self.Window.set_skip_taskbar_hint(False) self.Window.unstick() self.Window.fullscreen() # change Close/Menu button in fullscreen-mode self.ButtonClose.set_no_show_all(True) self.ButtonClose.hide() self.Window.show_all() except: import traceback traceback.print_exc(file=sys.stdout) # dummy return return True def CreateServerVBox(self, server_name=None, output=None): """ creates one VBox for one server """ # get the servers alphabetically sorted server = self.output.servers[server_name] # put all infos into one VBox object servervbox = ServerVBox(output=self.output, server=server) #servervbox.Label.set_markup('%s@%s' % (server.get_username(), server.get_name())) # initialize servervbox servervbox.initialize(server) # set no show all to be able to hide label and treeview if it is empty in case of no hassle servervbox.set_no_show_all(True) # connect buttons with actions # open Nagios main page in your favorite web browser when nagios button is clicked servervbox.ButtonMonitor.connect("clicked", server.OpenBrowser, "monitor", self.output) # open Nagios services in your favorite web browser when service button is clicked servervbox.ButtonServices.connect("clicked", server.OpenBrowser, "services", self.output) # open Nagios hosts in your favorite web browser when hosts button is clicked servervbox.ButtonHosts.connect("clicked", server.OpenBrowser, "hosts", self.output) # open Nagios history in your favorite web browser when hosts button is clicked servervbox.ButtonHistory.connect("clicked", server.OpenBrowser, "history", self.output) # OK button for monitor credentials refreshment or when "Enter" being pressed in password field servervbox.AuthButtonOK.connect("clicked", servervbox.AuthOK, server) # jump to password entry field if Return has been pressed on username entry field servervbox.AuthEntryUsername.connect("key-release-event", servervbox.AuthUsername) # for some reason signal "editing done" does not work so we need to check if Return has been pressed servervbox.AuthEntryPassword.connect("key-release-event", servervbox.AuthPassword, server) # windows workaround - see above # connect Server_EventBox with leave-notify-event to get popwin popping down when leaving it servervbox.Server_EventBox.connect("leave-notify-event", self.PopDown) # sorry folks, but this only works at the border of the treeviews servervbox.TreeView.connect("leave-notify-event", self.PopDown) # connect the treeviews of the servers to mouse clicks servervbox.TreeView.connect("button-press-event", servervbox.TreeviewPopupMenu, servervbox.TreeView, self.output.servers[server.get_name()]) """ # at the moment this feature does not work yet # Check_MK special feature: easily switch between private and overall view if server.type == "Check_MK Multisite": servervbox.HBoxCheckMK.set_no_show_all(False) servervbox.HBoxCheckMK.show_all() servervbox.CheckButtonCheckMKVisibility.set_active(bool(self.conf.only_my_issues)) servervbox.CheckButtonCheckMKVisibility.connect("clicked", server.ToggleVisibility) else: servervbox.HBoxCheckMK.set_no_show_all(True) servervbox.HBoxCheckMK.hide_all() """ return servervbox def PopUp(self, widget=None, event=None): """ pop up popwin """ # when popwin is showable and label is not "UP" popwin will be showed - # otherwise there is no sense in showing an empty popwin # for some reason the painting will lag behind popping up popwin if not getting resized twice - # seems like a strange workaround if self.showPopwin and not self.output.status_ok and self.output.conf.GetNumberOfEnabledMonitors() > 0: if len(self.output.GUILock) == 0 or self.output.GUILock.has_key("Popwin"): self.output.statusbar.Moving = False self.Window.show_all() self.Window.set_visible(True) # position and resize... self.calculate_coordinates = True self.Resize() # set combobox to default value self.ComboboxMonitor.set_active(0) # switch off Notification self.output.NotificationOff() # register as open window # use gobject.idle_add() to be thread safe gobject.idle_add(self.output.AddGUILock, str(self.__class__.__name__)) # position and resize... self.calculate_coordinates = True self.Resize() def RefreshFullscreen(self, widget=None, event=None): """ refresh fullscreen window """ # get current monitor's settings screenx0, screeny0, screenwidth, screenheight = self.output.monitors[int(self.conf.fullscreen_display)] # limit size of scrolled vbox vboxwidth, vboxheight = self.ScrolledVBox.size_request() # VBox should be as wide as the screen # https://github.com/HenriWahl/Nagstamon/issues/100 vboxwidth = screenwidth # get dimensions of top button bar self.buttonswidth, self.buttonsheight = self.HBoxAllButtons.size_request() # later GNOME might need some extra heightbuffer if using dual screen if vboxheight > screenheight - self.buttonsheight- self.heightbuffer_internal: # helpless attempt to get window fitting on screen if not maximized on newer unixoid DEs by doubling # external heightbuffer # leads to silly grey unused whitespace on GNOME3 dualmonitor, but there is still some information visisble.. # let's call this a feature no bug vboxheight = screenheight - self.buttonsheight - self.heightbuffer_internal else: # avoid silly scrollbar vboxheight += self.heightbuffer_internal #self.ScrolledWindow.set_size_request(-1, vboxheight) self.ScrolledWindow.set_size_request(vboxwidth, vboxheight) # even if fullscreen this is necessary self.Window.set_size_request(self.buttonswidth, -1) self.Window.show_all() self.Window.set_visible(True) # return False to get removed as gobject idle source return False def LeavePopWin(self, widget=None, event=None): """ when pointer leaves popwin the pointer_left_popwin flag has to be set to avoid popwin artefacts when hovering over statusbar """ self.pointer_left_popwin = True self.PopDown(widget, event) # after a shortdelay set pointer_left_popwin back to false, in the meantime # there will be no extra flickering popwin coming from hovered statusbar gobject.timeout_add(250, self.SetPointerLeftPopwinFalse) def SetPointerLeftPopwinFalse(self): """ function to be called by gobject. """ self.pointer_left_popwin = False return False def PopDown(self, widget=None, event=None): """ close popwin when it should closed it must be checked if the pointer is outside the popwin to prevent it closing when not necessary/desired """ if str(self.output.conf.fullscreen) == "False": # catch Exception try: # access to rootwindow to get the pointers coordinates rootwin = self.output.statusbar.StatusBar.get_screen().get_root_window() # get position of the pointer mousex, mousey, foo = rootwin.get_pointer() # get position of popwin popwinx0, popwiny0 = self.Window.get_position() # actualize values for width and height self.popwinwidth, self.popwinheight = self.Window.get_size() # If pointer is outside popwin close it # to support Windows(TM)'s slow and event-loosing behaviour use some margin (10px) to be more tolerant to get popwin closed # y-axis dooes not get extra 10 px on top for sake of combobox and x-axis on right side not because of scrollbar - # so I wonder if it has any use left... if str(self.conf.close_details_hover) == "True": if mousex <= popwinx0 + 10 or mousex >= (popwinx0 + self.popwinwidth) or mousey <= popwiny0 or mousey >= (popwiny0 + self.popwinheight) - 10 : self.Close() except: import traceback traceback.print_exc(file=sys.stdout) def Close(self, widget=None): """ hide popwin """ if str(self.output.conf.fullscreen) == "False": # unregister popwin - seems to be called even if popwin is not open so check before unregistering if self.output.GUILock.has_key("Popwin"): # use gobject.idle_add() to be thread safe gobject.idle_add(self.output.DeleteGUILock, self.__class__.__name__) # reset mousex and mousey coordinates for Windows popwin workaround self.mousex, self.mousey = 0, 0 self.Window.set_visible(False) # notification off because user had a look to hosts/services recently self.output.NotificationOff() # set all flagged-as-fresh-events to un-fresh self.output.UnfreshEventHistory() def Resize(self): """ calculate popwin dimensions depending on the amount of information displayed in scrollbox only if popwin is visible """ # the popwin should always pop up near the systray/desktop status bar, therefore we # need to find out its position # get dimensions of statusbar if str(self.conf.icon_in_systray) == "True": statusbarwidth, statusbarheight = 25, 25 else: statusbarwidth, statusbarheight = self.output.statusbar.StatusBar.get_size() # to avoid jumping popwin when statusbar changes dimensions set width fixed statusbarwidth = 320 if self.calculate_coordinates == True and str(self.conf.appindicator) == "False": # check if icon in systray or statusbar if str(self.conf.icon_in_systray) == "True": # trayicon seems not to have a .get_pointer() method so we use # its geometry information """ if platform.system() == "Windows": # otherwise this does not work in windows #if self.mousex == self.mousey == 0: # rootwin = self.output.statusbar.StatusBar.get_screen().get_root_window() # self.mousex, self.mousey, foo = rootwin.get_pointer() mousex, mousey = self.mousex, self.mousey statusbar_mousex, statusbar_mousey = 0, int(self.conf.systray_popup_offset) else: mousex, mousey, foo, bar = self.output.statusbar.SysTray.get_geometry()[1] statusbar_mousex, statusbar_mousey = 0, int(self.conf.systray_popup_offset) """ # regardless of the platform the fix for https://github.com/HenriWahl/Nagstamon/issues/63 # to use self.mousex and self.mousey makes things easier mousex, mousey = self.mousex, self.mousey statusbar_mousex, statusbar_mousey = 0, int(self.conf.systray_popup_offset) # set monitor for later applying the correct monitor geometry self.output.current_monitor = self.output.statusbar.StatusBar.get_screen().get_monitor_at_point(mousex, mousey) statusbarx0 = mousex - statusbar_mousex statusbary0 = mousey - statusbar_mousey else: statusbarx0, statusbary0 = self.output.statusbar.StatusBar.get_position() # set monitor for later applying the correct monitor geometry self.output.current_monitor = self.output.statusbar.StatusBar.get_screen().get_monitor_at_point(\ statusbarx0+statusbarwidth/2, statusbary0+statusbarheight/2) # save trayicon x0 and y0 in self.statusbar self.output.statusbar.StatusBar.x0 = statusbarx0 self.output.statusbar.StatusBar.y0 = statusbary0 # set back to False to do no recalculation of coordinates as long as popwin is opened self.calculate_coordinates = False else: if str(self.conf.appindicator) == "True": rootwin = self.output.appindicator.Menu_Nagstamon.get_screen().get_root_window() mousex, mousey, foo = rootwin.get_pointer() # set monitor for later applying the correct monitor geometry self.output.current_monitor = self.output.appindicator.Menu_Nagstamon.get_screen().get_monitor_at_point(mousex, mousey) # maybe more confusing but for not having rewrite too much code the statusbar*0 variables # are reused here self.popwinx0, dummy, screenwidth, dummy = self.output.monitors[self.output.current_monitor] # putting the "statusbar" into the farest right edge of the screen to get the popwin into that corner self.popwinx0 = screenwidth + self.popwinx0 else: # use previously saved values for x0 and y0 in case popwin is still/already open statusbarx0 = self.output.statusbar.StatusBar.x0 statusbary0 = self.output.statusbar.StatusBar.y0 # get current monitor's settings # screeny0 might be important on more-than-one-monitor-setups where it will not be 0 screenx0, screeny0, screenwidth, screenheight = self.output.monitors[self.output.current_monitor] # limit size of treeview treeviewwidth, treeviewheight = self.ScrolledVBox.size_request() if treeviewwidth > screenwidth: treeviewwidth = screenwidth # get dimensions of top button bar self.buttonswidth, self.buttonsheight = self.HBoxAllButtons.size_request() # later GNOME might need some extra heightbuffer if using dual screen if treeviewheight > screenheight - self.buttonsheight - statusbarheight - self.heightbuffer_external: treeviewheight = screenheight - self.buttonsheight - statusbarheight - self.heightbuffer_external else: # avoid silly scrollbar treeviewheight += self.heightbuffer_internal #### after having determined dimensions of scrolling area apply them ###self.ScrolledWindow.set_size_request(treeviewwidth, treeviewheight) # care about the height of the buttons self.popwinwidth, self.popwinheight = treeviewwidth, treeviewheight + self.buttonsheight # if popwinwidth is to small the buttons inside could be scrambled, so we give # it a minimum width from head buttons if self.popwinwidth < self.buttonswidth: self.popwinwidth = self.buttonswidth # if popwin is too wide cut it down to screen width - in case of AppIndicator use keep some space for ugly Ubuntu dock if str(self.conf.appindicator) == "True": if self.popwinwidth > screenwidth - 100: self.popwinwidth = screenwidth - 100 # fixed x0 coordinate self.popwinx0 = screenwidth - self.popwinwidth + screenx0 # make room for menu bar of Ubuntu if self.popwinheight >= screenheight - 25: treeviewheight += -25 self.popwinheight = screenheight - 25 # place popup unfer menu bar in Ubuntu self.popwiny0 = screeny0 + 25 else: if self.popwinwidth > screenwidth: self.popwinwidth = screenwidth # if statusbar/trayicon stays in upper half of screen, popwin pops up BELOW statusbar/trayicon # take into account different y0 on multiple monitors, otherwise the popwin might be scretched somehow if (statusbary0 - self.output.monitors[self.output.current_monitor][1] + statusbarheight - screeny0) < (screenheight / 2): # if popwin is too large it gets cut at lower end of screen # take into account different y0 on multiple monitors, otherwise the popwin might be scretched somehow if (statusbary0 - self.output.monitors[self.output.current_monitor][1] +\ self.popwinheight + statusbarheight) > screenheight: treeviewheight = screenheight - (statusbary0 + statusbarheight + self.buttonsheight) + screeny0 self.popwinheight = screenheight - statusbarheight - statusbary0 + screeny0 self.popwiny0 = statusbary0 + statusbarheight # else do not relate to screen dimensions but own widgets ones else: self.popwinheight = treeviewheight + self.buttonsheight self.popwiny0 = statusbary0 + statusbarheight # if it stays in lower half of screen, popwin pops up ABOVE statusbar/trayicon else: # if popwin is too large it gets cut at 0 if (statusbary0 - self.popwinheight - self.heightbuffer_external) <= screeny0: treeviewheight = statusbary0 - self.buttonsheight - statusbarheight - screeny0 - self.heightbuffer_internal self.popwinheight = statusbary0 - screeny0 - self.heightbuffer_external self.popwiny0 = screeny0 + self.heightbuffer_external # otherwise use own widgets for sizing else: self.popwinheight = treeviewheight + self.buttonsheight self.popwiny0 = statusbary0 - self.popwinheight # decide x position of popwin if (statusbarx0) + statusbarwidth / 2 + (self.popwinwidth) / 2 > (screenwidth + screenx0): self.popwinx0 = screenwidth - self.popwinwidth + screenx0 elif (statusbarx0 + statusbarwidth / 2)- self.popwinwidth / 2 < screenx0: self.popwinx0 = screenx0 else: self.popwinx0 = statusbarx0 + (screenx0 + statusbarwidth) / 2 - (self.popwinwidth + screenx0) / 2 # after having determined dimensions of scrolling area apply them self.ScrolledWindow.set_size_request(treeviewwidth, treeviewheight) # set size request of popwin self.Window.set_size_request(self.popwinwidth, self.popwinheight) if self.Window.get_properties("visible")[0] == True: # avoid resizing artefacts when popwin keeps opened introduced in 0.9.10 real_winwidth, real_winheight = self.Window.get_size() real_scrolledwinwidth, real_scrolledwinheight = self.ScrolledWindow.get_size_request() if real_scrolledwinheight + self.buttonsheight < real_winheight and not (self.popwinheight == treeviewheight + self.buttonsheight): self.Window.hide_all() self.Window.show_all() self.Window.window.move_resize(self.popwinx0, self.popwiny0, self.popwinwidth, self.popwinheight) # if popwin is misplaced please correct it here if self.Window.get_position() != (self.popwinx0, self.popwiny0): # would be nice if there could be any way to avoid flickering... # but move/resize only works after a hide_all()/show_all() mantra self.Window.hide_all() self.Window.show_all() self.Window.window.move_resize(self.popwinx0, self.popwiny0, self.popwinwidth, self.popwinheight) # statusbar pulls popwin to the top... with silly-windows-workaround(tm) included if str(self.conf.statusbar_floating) == "True": self.output.statusbar.Raise() return self.popwinx0, self.popwiny0, self.popwinwidth, self.popwinheight def setShowable(self, widget=None, event=None): """ stub method to set popwin showable after button-release-event after moving statusbar """ self.showPopwin = True def ComboboxClicked(self, widget=None): """ open web interface of selected server """ try: active = widget.get_active_iter() model = widget.get_model() self.output.servers[model.get_value(active, 0)].OpenBrowser(url_type="monitor", output=self.output) except: self.output.servers.values()[0].Error(sys.exc_info()) def UpdateStatus(self, server): """ Updates status field of a server """ # status field in server vbox in popwin try: # kick out final "\n" for nicer appearance self.ServerVBoxes[server.get_name()].LabelStatus.set_markup(' Status: %s %s' % (str(server.status), str(server.status_description).rsplit("\n", 1)[0])) except: server.Error(sys.exc_info()) # return False to get removed as gobject idle source return False def IsWanted(self): """ check if no other dialog/menu is shown which would not like to be covered by the popup window """ if len(self.output.GUILock) == 0 or "Popwin" in self.output.GUILock: return True else: return False def MenuPopUp(self, widget=None, event=None): """ popup menu for maximized overview window - instead of statusbar menu """ self.Menu.popup(None, None, None, event.button, event.time) def MenuResponse(self, widget, menu_entry): """ responses for the context menu for menu button in maximized popup status window """ if menu_entry == "Save position": self.conf.SaveConfig(output=self.output) if menu_entry == "About": self.output.AboutDialog() if menu_entry == "Exit": self.output.Exit(True) class ServerVBox(gtk.VBox): """ VBox which contains all infos about one monitor server: Name, Buttons, Treeview """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] # initalize VBox gtk.VBox.__init__(self) # elements of server info VBox self.Label = gtk.Label() self.Label.set_alignment(0,0) # once again a Windows(TM) workaround self.Server_EventBox = gtk.EventBox() # create icony buttons self.ButtonMonitor = ButtonWithIcon(output=self.output, label="Monitor", icon="nagios.png") self.ButtonHosts = ButtonWithIcon(output=self.output, label="Hosts", icon="hosts.png") self.ButtonServices = ButtonWithIcon(output=self.output, label="Services", icon="services.png") self.ButtonHistory = ButtonWithIcon(output=self.output, label="History", icon="history.png") """ # not yet working # Check_MK stuff self.CheckButtonCheckMKVisibility = gtk.CheckButton("Only my issues") """ # Label with status information self.LabelStatus = gtk.Label("") # order the elements # now vboxing the elements to add a line in case authentication failed - so the user should auth here again self.VBox = gtk.VBox() # first line for usual monitor shortlink buttons self.HBox = gtk.HBox() self.HBoxLeft = gtk.HBox() self.HBoxCheckMK = gtk.HBox() self.HBoxRight = gtk.HBox() self.HBoxLeft.add(self.Label) # leave some space around the label self.Label.set_padding(5, 5) self.HBoxLeft.add(self.ButtonMonitor) self.HBoxLeft.add(self.ButtonHosts) self.HBoxLeft.add(self.ButtonServices) self.HBoxLeft.add(self.ButtonHistory) """ # see above # Check_MK stuff self.HBoxCheckMK.add(gtk.VSeparator()) self.HBoxCheckMK.add(self.CheckButtonCheckMKVisibility) self.HBoxLeft.add(self.HBoxCheckMK) """ # Status info self.HBoxLeft.add(gtk.VSeparator()) self.HBoxLeft.add(self.LabelStatus) self.AlignmentLeft = gtk.Alignment(xalign=0, xscale=0.0, yalign=0) self.AlignmentLeft.add(self.HBoxLeft) self.AlignmentRight = gtk.Alignment(xalign=0, xscale=0.0, yalign=0.5) self.AlignmentRight.add(self.HBoxRight) self.HBox.add(self.AlignmentLeft) self.HBox.add(self.AlignmentRight) self.VBox.add(self.HBox) # Auth line self.HBoxAuth = gtk.HBox() self.AuthLabelUsername = gtk.Label(" Username: ") self.AuthEntryUsername = gtk.Entry() self.AuthEntryUsername.set_can_focus(True) self.AuthLabelPassword = gtk.Label(" Password: ") self.AuthEntryPassword = gtk.Entry() self.AuthEntryPassword.set_visibility(False) self.AuthCheckbuttonSave = gtk.CheckButton("Save password ") self.AuthButtonOK = gtk.Button(" OK ") self.HBoxAuth.add(self.AuthLabelUsername) self.HBoxAuth.add(self.AuthEntryUsername) self.HBoxAuth.add(self.AuthLabelPassword) self.HBoxAuth.add(self.AuthEntryPassword) self.HBoxAuth.add(self.AuthCheckbuttonSave) self.HBoxAuth.add(self.AuthButtonOK) self.AlignmentAuth = gtk.Alignment(xalign=0, xscale=0.0, yalign=0) self.AlignmentAuth.add(self.HBoxAuth) self.VBox.add(self.AlignmentAuth) # start with hidden auth as default self.HBoxAuth.set_no_show_all(True) self.HBoxAuth.hide_all() self.Server_EventBox.add(self.VBox) self.add(self.Server_EventBox) # new TreeView handling, not generating new items with every refresh cycle self.server.TreeView = gtk.TreeView() # enable hover effect self.server.TreeView.set_hover_selection(True) """ # tooltips or not if str(self.output.conf.show_tooltips) == "True": self.server.TreeView.set_has_tooltip(True) self.server.TreeView.set_tooltip_column(7) else: self.server.TreeView.set_has_tooltip(False) # enable grid lines if str(self.output.conf.show_grid) == "True": self.server.TreeView.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH) else: self.server.TreeView.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_NONE) """ # Liststore self.server.ListStore = gtk.ListStore(*self.output.LISTSTORE_COLUMNS) # offset to access host and service flag icons separately, stored in grand liststore # may grow with more supported flags offset_img = {0:0, 1:len(self.output.STATE_ICONS)} # offset for alternate column colors could increase readability # even and odd columns are calculated by column number offset_color = {0:9, 1:10} for s, column in enumerate(self.server.COLUMNS): tab_column = gtk.TreeViewColumn(column.get_label()) self.server.TreeView.append_column(tab_column) # the first and second column hold hosts and service name which will get acknowledged/downtime flag # indicators added if s in [0, 1]: # pixbuf for little icon cell_img_fresh = gtk.CellRendererPixbuf() cell_img_ack = gtk.CellRendererPixbuf() cell_img_down = gtk.CellRendererPixbuf() cell_img_flap = gtk.CellRendererPixbuf() cell_img_pass = gtk.CellRendererPixbuf() # host/service name cell_txt = gtk.CellRendererText() # stuff all renderers into one cell tab_column.pack_start(cell_txt, False) tab_column.pack_start(cell_img_fresh, False) tab_column.pack_start(cell_img_ack, False) tab_column.pack_start(cell_img_down, False) tab_column.pack_start(cell_img_flap, False) tab_column.pack_start(cell_img_pass, False) # set text from liststore and flag icons if existing # why ever, in Windows(TM) the background looks better if applied separately # to be honest, even looks better in Linux tab_column.set_attributes(cell_txt, foreground=8, text=s) tab_column.add_attribute(cell_txt, "cell-background", offset_color[s % 2]) tab_column.set_attributes(cell_img_fresh, pixbuf=11+offset_img[s]) tab_column.add_attribute(cell_img_fresh, "cell-background", offset_color[s % 2]) tab_column.set_attributes(cell_img_ack, pixbuf=12+offset_img[s]) tab_column.add_attribute(cell_img_ack, "cell-background", offset_color[s % 2]) tab_column.set_attributes(cell_img_down, pixbuf=13+offset_img[s]) tab_column.add_attribute(cell_img_down, "cell-background", offset_color[s % 2]) tab_column.set_attributes(cell_img_flap, pixbuf=14+offset_img[s]) tab_column.add_attribute(cell_img_flap, "cell-background", offset_color[s % 2]) tab_column.set_attributes(cell_img_pass, pixbuf=15+offset_img[s]) tab_column.add_attribute(cell_img_pass, "cell-background", offset_color[s % 2]) tab_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) else: # normal way for all other columns cell_txt = gtk.CellRendererText() tab_column.pack_start(cell_txt, False) tab_column.set_attributes(cell_txt, foreground=8, text=s) tab_column.add_attribute(cell_txt, "cell-background", offset_color[s % 2 ]) tab_column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) # set customized sorting if column.has_customized_sorting(): self.server.ListStore.set_sort_func(s, column.sort_function, s) # make table sortable by clicking on column headers tab_column.set_clickable(True) tab_column.connect('clicked', self.output.on_column_header_click, s, self.server.ListStore, self.server) # the whole TreeView memory leaky complex... self.TreeView = self.server.TreeView self.ListStore = self.server.ListStore self.add(self.TreeView) def initialize(self, server): """ set settings, to be used by __init__ and after changed settings in Settings dialog """ # user@server info label self.Label.set_markup('%s@%s' % (server.get_username(), server.get_name())) # tooltips or not if str(self.output.conf.show_tooltips) == "True": self.server.TreeView.set_tooltip_column(7) else: self.server.TreeView.set_tooltip_column(-1) # enable grid lines if str(self.output.conf.show_grid) == "True": self.server.TreeView.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH) else: self.server.TreeView.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_NONE) def TreeviewPopupMenu(self, widget, event, treeview, server): """ context menu for treeview detailed status items """ # catch exception in case of clicking outside treeview try: # get path to clicked cell path, obj, x, y = treeview.get_path_at_pos(int(event.x), int(event.y)) # access content of rendered view model via normal python lists self.miserable_server = server self.miserable_host = treeview.get_model()[path[0]][server.HOST_COLUMN_ID] self.miserable_service = treeview.get_model()[path[0]][server.SERVICE_COLUMN_ID] self.miserable_status_info = treeview.get_model()[path[0]][server.STATUS_INFO_COLUMN_ID] # context menu for detailed status overview, opens with a mouse click onto a listed item self.popupmenu = gtk.Menu() # add custom actions actions_list=list(self.output.conf.actions) actions_list.sort(key=str.lower) for a in actions_list: # shortcut for next lines action = self.output.conf.actions[a] if str(action.enabled) == "True" and action.monitor_type in ["", self.server.TYPE] : # menu item visibility flag item_visible = False # check if clicked line is a service or host # if it is check if the action is targeted on hosts or services if self.miserable_service: if str(action.filter_target_service) == "True": # only check if there is some to check if str(action.re_host_enabled) == "True": if Actions.IsFoundByRE(self.miserable_host,\ action.re_host_pattern,\ action.re_host_reverse): item_visible = True # dito if str(action.re_service_enabled) == "True": if Actions.IsFoundByRE(self.miserable_service,\ action.re_service_pattern,\ action.re_service_reverse): item_visible = True # dito if str(action.re_status_information_enabled) == "True": if Actions.IsFoundByRE(self.miserable_service,\ action.re_status_information_pattern,\ action.re_status_information_reverse): item_visible = True # fallback if no regexp is selected if str(action.re_host_enabled) == str(action.re_service_enabled) == str(action.re_status_information_enabled) == "False": item_visible = True else: # hosts should only care about host specific actions, no services if str(action.filter_target_host) == "True": if str(action.re_host_enabled) == "True": if Actions.IsFoundByRE(self.miserable_host,\ action.re_host_pattern,\ action.re_host_reverse): item_visible = True else: # a non specific action will be displayed per default item_visible = True else: item_visible = False # populate context menu with service actions if item_visible == True: menu_item = gtk.MenuItem(a) menu_item.connect("activate", self.TreeviewPopupMenuResponse, a) self.popupmenu.append(menu_item) del action, item_visible # add "Edit actions..." menu entry menu_item = gtk.MenuItem("Edit actions...") menu_item.connect("activate", self.TreeviewPopupMenuResponse, "Edit actions...") self.popupmenu.append(menu_item) # add separator to separate between connections and actions self.popupmenu.append(gtk.SeparatorMenuItem()) # after the separator add actions # available default menu actions are monitor server dependent for i in self.server.MENU_ACTIONS: # sometimes menu does not open due to "Recheck" so catch that exception try: # recheck is not necessary for passive set checks if i == "Recheck" and self.miserable_service\ and server.hosts[self.miserable_host].services[self.miserable_service].is_passive_only(): pass else: menu_item = gtk.MenuItem(i) menu_item.connect("activate", self.TreeviewPopupMenuResponse, i) self.popupmenu.append(menu_item) except: menu_item = gtk.MenuItem(i) menu_item.connect("activate", self.TreeviewPopupMenuResponse, i) self.popupmenu.append(menu_item) self.popupmenu.show_all() self.popupmenu.popup(None, None, None, event.button, event.time) # silly Windows(TM) workaround to keep menu above popwin self.popupmenu.window.set_keep_above(True) except: import traceback traceback.print_exc(file=sys.stdout) def TreeviewPopupMenuResponse(self, widget, remoteservice): """ responses to the menu items commands get called by subprocess.Popen to beware nagstamon of hanging while waiting for the called command exit code the requested command and its arguments are given by a list """ # closing popwin is innecessary in case of rechecking, otherwise it must be done # because the dialog/app window will stay under the popwin if remoteservice in ["Acknowledge", "Monitor", "Downtime", "Submit check result", "Edit actions..."]: self.output.popwin.Close() #debug if str(self.output.conf.debug_mode) == "True": self.miserable_server.Debug(server=self.miserable_server.get_name(),\ host=self.miserable_host,\ service=self.miserable_service,\ debug="Clicked context menu: " + remoteservice) # choose appropriate service for menu entry # it seems to be more responsive especially while rechecking if every service # looks for its own for the miserable host's ip if it is needed try: # custom actions if remoteservice in self.output.conf.actions: # let the thread do the work action = Actions.Action(action=self.output.conf.actions[remoteservice],\ conf=self.output.conf,\ server=self.miserable_server,\ host=self.miserable_host,\ service=self.miserable_service,\ status_info=self.miserable_status_info) # if action wants a closed powin it should be closed if str(self.output.conf.actions[remoteservice].close_popwin) == "True": self.output.popwin.Close() # Action! action.start() elif remoteservice == "Edit actions...": # open actions settings self.output.GetDialog(dialog="Settings", servers=self.output.servers, output=self.output, conf=self.output.conf, first_page="Actions") elif remoteservice == "Monitor": # let Actions.TreeViewNagios do the work to open a webbrowser with nagios informations Actions.TreeViewNagios(self.miserable_server, self.miserable_host, self.miserable_service) elif remoteservice == "Recheck": # start new rechecking thread recheck = Actions.Recheck(server=self.miserable_server, host=self.miserable_host, service=self.miserable_service) recheck.start() elif remoteservice == "Acknowledge": self.output.AcknowledgeDialogShow(server=self.miserable_server, host=self.miserable_host, service=self.miserable_service) elif remoteservice == "Submit check result": self.output.SubmitCheckResultDialogShow(server=self.miserable_server, host=self.miserable_host, service=self.miserable_service) elif remoteservice == "Downtime": self.output.DowntimeDialogShow(server=self.miserable_server, host=self.miserable_host, service=self.miserable_service) # close popwin self.output.popwin.PopDown() except Exception, err: self.output.Dialog(message=err) def AuthOK(self, widget, server): """ use given auth informations """ server.username, server.password = self.AuthEntryUsername.get_text(), self.AuthEntryPassword.get_text() server.refresh_authentication = False if self.AuthCheckbuttonSave.get_active() == True: # store authentication information in config server.conf.servers[server.get_name()].username = server.username server.conf.servers[server.get_name()].password = server.password server.conf.servers[server.get_name()].save_password = True server.conf.SaveConfig(server=server) self.HBoxAuth.hide_all() self.HBoxAuth.set_no_show_all(True) # refresh server label self.Label.set_markup('%s@%s' % (server.get_username(), server.get_name())) server.status = "Trying to reauthenticate..." server.status_description = "" self.output.popwin.UpdateStatus(server) self.output.popwin.Resize() def AuthUsername(self, widget, event): """ if Return key has been pressed in password entry field interprete this as OK button being pressed """ if gtk.gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: self.AuthEntryPassword.grab_focus() def AuthPassword(self, widget, event, server): """ if Return key has been pressed in password entry field interprete this as OK button being pressed """ if gtk.gdk.keyval_name(event.keyval) in ["Return", "KP_Enter"]: self.AuthOK(widget, server) class AppIndicator(object): """ Ubuntu AppIndicator management class """ def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] self.Indicator = appindicator.Indicator("Nagstamon", self.output.Resources + os.sep + "nagstamon_appindicator" +\ self.output.BitmapSuffix, appindicator.CATEGORY_APPLICATION_STATUS) # define all items on AppIndicator menu, which might be switched on and off depending of their relevance self.Menu = gtk.Menu() # Nagstamon Submenu self.Menu_Nagstamon = gtk.MenuItem("Nagstamon") self.Menu_Nagstamon.set_submenu(self.output.statusbar.Menu) self.Menu_Separator = gtk.SeparatorMenuItem() # Status menu items self.Menu_DOWN = gtk.MenuItem("") self.Menu_DOWN.connect("activate", self.output.popwin.PopUp) self.Menu_UNREACHABLE = gtk.MenuItem("") self.Menu_UNREACHABLE.connect("activate", self.output.popwin.PopUp) self.Menu_CRITICAL = gtk.MenuItem("") self.Menu_CRITICAL.connect("activate", self.output.popwin.PopUp) self.Menu_UNKNOWN = gtk.MenuItem("") self.Menu_UNKNOWN.connect("activate", self.output.popwin.PopUp) self.Menu_WARNING = gtk.MenuItem("") self.Menu_WARNING.connect("activate", self.output.popwin.PopUp) # show detail popup, same effect as clicking one f the above self.Menu_ShowDetails = gtk.MenuItem("Show details...") self.Menu_ShowDetails.connect("activate", self.output.popwin.PopUp) self.Menu_OK = gtk.MenuItem("OK") self.Menu_OK.connect("activate", self.OK) self.Menu.append(self.Menu_Nagstamon) self.Menu.append(self.Menu_Separator) self.Menu.append(self.Menu_DOWN) self.Menu.append(self.Menu_UNREACHABLE) self.Menu.append(self.Menu_CRITICAL) self.Menu.append(self.Menu_UNKNOWN) self.Menu.append(self.Menu_WARNING) self.Menu.append(self.Menu_ShowDetails) self.Menu.append(self.Menu_OK) self.Menu_Nagstamon.show() self.Menu_Separator.show() self.Menu_ShowDetails.show() self.Menu.show() self.Indicator.set_menu(self.Menu) # display AppIndicator only if configured if str(self.conf.appindicator) == "True": self.Indicator.set_status(appindicator.STATUS_ACTIVE) else: self.Indicator.set_status(appindicator.STATUS_PASSIVE) # flash flag evaluated in notification thread self.Flashing = False def Flash(self): """ Flash in case of reason to do so """ if self.Indicator.get_status() == appindicator.STATUS_ATTENTION: self.Indicator.set_status(appindicator.STATUS_ACTIVE) else: self.Indicator.set_status(appindicator.STATUS_ATTENTION) # return False to get removed as gobject idle source return False def OK(self, dummy=None): """ action for OK menu entry, to be triggered if notification is acknowledged """ self.Menu_OK.hide() self.output.NotificationOff() self.Indicator.set_status(appindicator.STATUS_ATTENTION) self.output.popwin.Close() class Settings(object): """ settings dialog as object, may lead to less mess """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # if not given default tab is empty if not "first_page" in kwds: self.first_page = "Servers" # set the gtkbuilder files self.builderfile = self.output.Resources + os.sep + "settings_dialog.ui" self.builder = gtk.Builder() self.builder.add_from_file(self.builderfile) self.dialog = self.builder.get_object("settings_dialog") # little feedback store for servers and actions treeviews self.selected_server = None self.selected_action = None # use connect_signals to assign methods to handlers handlers_dict = { "button_ok_clicked": self.OK, "settings_dialog_close": self.Cancel, "button_cancel_clicked": self.Cancel, "button_new_server": lambda n: self.output.GetDialog(dialog="NewServer", servers=self.servers, output=self.output, settingsdialog=self, conf=self.conf), "button_edit_server": lambda e: self.output.GetDialog(dialog="EditServer", servers=self.servers, output=self.output, selected_server=self.selected_server, settingsdialog=self, conf=self.conf), "button_copy_server": lambda e: self.output.GetDialog(dialog="CopyServer", servers=self.servers, output=self.output, selected_server=self.selected_server, settingsdialog=self, conf=self.conf), "button_delete_server": lambda d: self.DeleteServer(self.selected_server, self.conf.servers), "button_check_for_new_version_now": self.CheckForNewVersionNow, "checkbutton_enable_notification": self.ToggleNotification, "checkbutton_enable_sound": self.ToggleSoundOptions, "togglebutton_use_custom_sounds": self.ToggleCustomSoundOptions, "checkbutton_re_host_enabled": self.ToggleREHostOptions, "checkbutton_re_service_enabled": self.ToggleREServiceOptions, "checkbutton_re_status_information_enabled": self.ToggleREStatusInformationOptions, "checkbutton_re_criticality_enabled": self.ToggleRECriticalityOptions, "button_play_sound": self.PlaySound, "checkbutton_debug_mode": self.ToggleDebugOptions, "checkbutton_debug_to_file": self.ToggleDebugOptions, "button_colors_default": self.ColorsDefault, "button_colors_reset": self.ColorsReset, "color-set": self.ColorsPreview, "radiobutton_icon_in_systray_toggled": self.ToggleSystrayPopupOffset, "radiobutton_fullscreen_toggled": self.ToggleFullscreenDisplay, "notification_actions": self.ToggleNotificationActions, "notification_custom_action": self.ToggleNotificationCustomAction, "notification_action_warning": self.ToggleNotificationActionWarning, "notification_action_critical": self.ToggleNotificationActionCritical, "notification_action_down": self.ToggleNotificationActionDown, "notification_action_ok": self.ToggleNotificationActionOk, "button_help_notification_actions_clicked": self.ToggleNotificationActionsHelp, "button_help_notification_custom_actions_clicked": self.ToggleNotificationCustomActionsHelp, "button_new_action": lambda a: self.output.GetDialog(dialog="NewAction", output=self.output, settingsdialog=self, conf=self.conf), "button_edit_action": lambda e: self.output.GetDialog(dialog="EditAction", output=self.output, selected_action=self.selected_action, settingsdialog=self, conf=self.conf), "button_copy_action": lambda e: self.output.GetDialog(dialog="CopyAction", output=self.output, selected_action=self.selected_action, settingsdialog=self, conf=self.conf), "button_delete_action": lambda d: self.DeleteAction(self.selected_action, self.conf.actions), } self.builder.connect_signals(handlers_dict) # keystore option has to be set/unset before it gets overwritten by the following loops self.conf.keyring_available = self.conf.KeyringAvailable() self.ToggleSystemKeyring() keys = self.conf.__dict__.keys() # walk through all relevant input types to fill dialog with existing settings for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_", "input_spinbutton_", "input_filechooser_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to fill it with desired content # so we try them all, one of them should work # help gtk.filechooser on windows if str(self.conf.__dict__[key]) == "None": self.conf.__dict__[key] = None try: # filechooser j.set_filename(self.conf.__dict__[key]) except: pass try: j.set_text(self.conf.__dict__[key]) except: pass try: if str(self.conf.__dict__[key]) == "True": j.set_active(True) if str(self.conf.__dict__[key]) == "False": j.set_active(False) except: pass try: j.set_value(int(self.conf.__dict__[key])) except: pass # hide open popwin in try/except clause because at first start there # cannot be a popwin object try: self.output.popwin.Close() except: pass # set title of settings dialog containing version number self.dialog.set_title(self.output.name + " " + self.output.version + " settings") # workaround for gazpacho-made glade-file - dunno why tab labels do not get named as they should be self.notebook = self.builder.get_object("notebook") notebook_tabs = ["Servers", "Display", "Filters", "Actions", "Notifications", "Colors", "Defaults"] # now this presumably not necessary anymore workaround even gets extended as # determine-first-page-mechanism used for acknowledment dialog settings button page = 0 for c in self.notebook.get_children(): if notebook_tabs[0] == self.first_page: self.notebook.set_current_page(page) self.notebook.set_tab_label_text(c, notebook_tabs.pop(0)) page += 1 # fill treeviews self.FillTreeView("servers_treeview", self.conf.servers, "Servers", "selected_server") self.FillTreeView("actions_treeview", self.conf.actions, "Actions", "selected_action") # set filters fore sound filechoosers filters = dict() # WAV files work on every platform filters["wav"] = gtk.FileFilter() filters["wav"].set_name("WAV files") filters["wav"].add_pattern("*.wav") filters["wav"].add_pattern("*.WAV") # OGG files are only usable on unixoid OSes: if not platform.system() == "Windows": filters["ogg"] = gtk.FileFilter() filters["ogg"].set_name("OGG files") filters["ogg"].add_pattern("*.ogg") filters["ogg"].add_pattern("*.OGG") for f in ["warning", "critical", "down"]: filechooser = self.builder.get_object("input_filechooser_notification_custom_sound_" + f) for f in filters: filechooser.add_filter(filters[f]) # for some reason does not show wanted effect filechooser.set_filter(filters["wav"]) # commit e1946ea33fefac6271d44eb44c05dd2c3ff5bfe9 from pull request by foscarini@github # offering sort order for status popup # default sort column field self.combo_default_sort_field = self.builder.get_object("input_combo_default_sort_field") combomodel_default_sort_field = gtk.ListStore(gobject.TYPE_STRING) crsf = gtk.CellRendererText() self.combo_default_sort_field.pack_start(crsf, True) self.combo_default_sort_field.set_attributes(crsf, text=0) for i in range(6): combomodel_default_sort_field.append((self.output.IDS_COLUMNS_MAP[i],)) self.combo_default_sort_field.set_model(combomodel_default_sort_field) self.combo_default_sort_field.set_active(self.output.COLUMNS_IDS_MAP[self.conf.default_sort_field]) # default column sort order combobox self.combo_default_sort_order = self.builder.get_object("input_combo_default_sort_order") combomodel_default_sort_order = gtk.ListStore(gobject.TYPE_STRING) crso = gtk.CellRendererText() self.combo_default_sort_order.pack_start(crso, True) self.combo_default_sort_order.set_attributes(crso, text=0) combomodel_default_sort_order.append(("Ascending" ,)) combomodel_default_sort_order.append(("Descending",)) self.combo_default_sort_order.set_model(combomodel_default_sort_order) self.combo_default_sort_order.set_active({"Ascending": 0, "Descending": 1}[self.conf.default_sort_order]) # fill fullscreen display combobox self.combo_fullscreen_display = self.builder.get_object("input_combo_fullscreen_display") combomodel_fullscreen_display = gtk.ListStore(gobject.TYPE_STRING) crfsd = gtk.CellRendererText() self.combo_fullscreen_display.pack_start(crfsd, True) self.combo_fullscreen_display.set_attributes(crfsd, text=0) for i in self.output.monitors: combomodel_fullscreen_display.append((str(i))) self.combo_fullscreen_display.set_model(combomodel_fullscreen_display) self.combo_fullscreen_display.set_active(int(self.conf.fullscreen_display)) # in case nagstamon runs the first time it should display a new server dialog if str(self.conf.unconfigured) == "True": self.output.statusbar.StatusBar.hide() self.output.GetDialog(dialog="NewServer", servers=self.servers, output=self.output, settingsdialog=self, conf=self.conf) # save settings self.conf.SaveConfig(output=self.output) # prepare colors and preview them self.ColorsReset() # disable non useful gui settings if platform.system() == "Darwin": # MacOS doesn't need any option because there is only floating statusbar possible self.builder.get_object("input_radiobutton_icon_in_systray").hide() self.builder.get_object("hbox_systray_popup_offset").hide() self.builder.get_object("input_radiobutton_statusbar_floating").hide() self.builder.get_object("label_appearance").hide() self.builder.get_object("input_radiobutton_fullscreen").hide() self.builder.get_object("input_combo_fullscreen_display").hide() self.builder.get_object("label_fullscreen_display").hide() self.builder.get_object("input_checkbutton_notification_desktop").hide() self.builder.get_object("input_radiobutton_appindicator").hide() # as of now there is no notification in Windows so disable it if platform.system() == "Windows": self.builder.get_object("input_checkbutton_notification_desktop").hide() self.builder.get_object("input_radiobutton_appindicator").hide() # libnotify-based desktop notification probably only available on Linux if not sys.modules.has_key("pynotify"): self.builder.get_object("input_checkbutton_notification_desktop").hide() # appindicator option is not needed on non-Ubuntuesque systems if not sys.modules.has_key("appindicator"): self.builder.get_object("input_radiobutton_appindicator").hide() # this should not be necessary, but for some reason the number of hours is 10 in unitialized state... :-( spinbutton = self.builder.get_object("input_spinbutton_defaults_downtime_duration_hours") spinbutton.set_value(int(self.conf.defaults_downtime_duration_hours)) spinbutton = self.builder.get_object("input_spinbutton_defaults_downtime_duration_minutes") spinbutton.set_value(int(self.conf.defaults_downtime_duration_minutes)) # store fullscreen state to avoif innecessary popwin flickering self.saved_fullscreen_state = str(self.conf.fullscreen) # initialize state of some GUI elements self.initialize() def show(self): # show filled settings dialog and wait thanks to gtk.run() self.dialog.run() # delete global open Windows entry gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def initialize(self): """ initialize some stuff at every call of this dialog """ # set first page of notebook tabs - meanwhile for some historic reason self.notebook.set_current_page(["Servers", "Display", "Filters", "Actions",\ "Notification", "Colors", "Defaults"].index(self.first_page)) # store fullscreen state to avoid innecessary popwin flickering self.saved_fullscreen_state = str(self.conf.fullscreen) # toggle regexp options self.ToggleREHostOptions() self.ToggleREServiceOptions() self.ToggleREStatusInformationOptions() # care about Centreon criticality filter self.ToggleRECriticalityFilter() self.ToggleRECriticalityOptions() # toggle debug options self.ToggleDebugOptions() # toggle sounds options self.ToggleSoundOptions() self.ToggleCustomSoundOptions() # toggle icon in systray popup offset self.ToggleSystrayPopupOffset() # toggle fullscreen display selection combobox self.ToggleFullscreenDisplay() # toggle notification action options self.ToggleNotificationActions() self.ToggleNotificationActionWarning() self.ToggleNotificationActionCritical() self.ToggleNotificationActionDown() self.ToggleNotificationActionOk() self.ToggleSystemKeyring() def FillTreeView(self, treeview_widget, items, column_string, selected_item): """ fill treeview containing items - has been for servers only before treeview_widget - string from gtk builder items - dictionary containing the to-be-listed items column_string - certain column name selected_item - property which stores the selected item """ # create a model for treeview where the table headers all are strings liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN) # to sort the monitor servers alphabetically make a sortable list of their names item_list = list(items) item_list.sort(key=str.lower) for item in item_list: iter = liststore.insert_before(None, None) liststore.set_value(iter, 0, item) if str(items[item].enabled) == "True": liststore.set_value(iter, 1, "black") liststore.set_value(iter, 2, False) else: liststore.set_value(iter, 1, "darkgrey") liststore.set_value(iter, 2, True) # give model to the view self.builder.get_object(treeview_widget).set_model(liststore) # render aka create table view renderer_text = gtk.CellRendererText() tab_column = gtk.TreeViewColumn(column_string, renderer_text, text=0, foreground=1, strikethrough=2) # somehow idiotic, but less effort... try to delete which column ever, to create a new one # this will throw an exception at the first time the options dialog is opened because no column exists try: self.builder.get_object(treeview_widget).remove_column(self.builder.get_object(treeview_widget).get_column(0)) except: pass self.builder.get_object(treeview_widget).append_column(tab_column) # in case there are no items yet because it runs the first time do a try-except try: # selected server to edit or delete, defaults to first one of server list self.__dict__[selected_item] = item_list[0] # select first entry self.builder.get_object(treeview_widget).set_cursor_on_cell((0,)) except: pass # connect treeview with mouseclicks self.builder.get_object(treeview_widget).connect("button-press-event", self.SelectedTreeviewItem, treeview_widget, selected_item) def SelectedTreeviewItem(self, widget, event, treeview_widget, selected_item): """ findout selected item in treeview, should NOT return anything because the treeview will be displayed buggy if it does """ try: # get path to clicked cell path, obj, x, y = self.builder.get_object(treeview_widget).get_path_at_pos(int(event.x), int(event.y)) # access content of rendered view model via normal python lists and put # it into Settings dictionary self.__dict__[selected_item] = self.builder.get_object(treeview_widget).get_model()[path[0]][0] except: pass def OK(self, widget): """ when dialog gets closed the content of its widgets gets put into the appropriate values of the config object after this the config file gets saved. """ keys = self.conf.__dict__.keys() for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_", "input_spinbutton_", "input_filechooser_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to get its content # so we try them all, one of them should work try: self.conf.__dict__[key] = j.get_text() except: try: self.conf.__dict__[key] = j.get_active() except: try: self.conf.__dict__[key] = int(j.get_value()) except: try: # filechooser self.conf.__dict__[key] = j.get_filename() except: pass # evaluate and apply colors for state in ["ok", "warning", "critical", "unknown", "unreachable", "down", "error"]: self.conf.__dict__["color_" + state + "_text"] = self.builder.get_object("input_colorbutton_" + state + "_text").get_color().to_string() self.conf.__dict__["color_" + state + "_background"] = self.builder.get_object("input_colorbutton_" + state + "_background").get_color().to_string() # add new color information to color dictionaries for cells to render self.output.TAB_FG_COLORS[state.upper()] = self.builder.get_object("input_colorbutton_" + state + "_text").get_color().to_string() self.output.TAB_BG_COLORS[state.upper()] = self.builder.get_object("input_colorbutton_" + state + "_background").get_color().to_string() # evaluate comboboxes self.conf.default_sort_field = self.combo_default_sort_field.get_active_text() self.conf.default_sort_order = self.combo_default_sort_order.get_active_text() self.conf.fullscreen_display = self.combo_fullscreen_display.get_active_text() # close popwin # catch Exception at first run when there cannot exist a popwin try: # only useful if not on first run if self.output.firstrun == False and self.conf.unconfigured == False: self.output.popwin.PopDown() except: import traceback traceback.print_exc(file=sys.stdout) if int(self.conf.update_interval_seconds) <= 0: self.conf.update_interval_seconds = 60 # save settings self.conf.SaveConfig(output=self.output) # catch exceptions in case of misconfiguration try: # now it is not the first run anymore self.output.firstrun = False self.conf.unconfigured = False gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() if str(self.conf.statusbar_floating) == "True": self.output.statusbar.StatusBar.show_all() self.output.statusbar.CalculateFontSize() else: self.output.statusbar.StatusBar.hide_all() if str(self.conf.icon_in_systray) == "True": self.output.statusbar.SysTray.set_visible(True) else: self.output.statusbar.SysTray.set_visible(False) # only if appindicator module exists if sys.modules.has_key("appindicator"): if str(self.conf.appindicator) == "True": self.output.appindicator.OK() else: self.output.appindicator.Indicator.set_status(appindicator.STATUS_PASSIVE) # in Windows the statusbar with gtk.gdk.WINDOW_TYPE_HINT_UTILITY places itself somewhere # this way it should be disciplined self.output.statusbar.StatusBar.move(int(self.conf.position_x), int(self.conf.position_y)) # popwin treatment # only change popwin if fullscreen mode is changed if self.saved_fullscreen_state != str(self.conf.fullscreen): self.output.popwin.SwitchMode() # apply settings for modified servers self.output.ApplyServerModifications() except: import traceback traceback.print_exc(file=sys.stdout) self.servers.values()[0].Error(sys.exc_info()) def Cancel(self, widget): """ settings dialog got cancelled """ # when getting cancelled at first run exit immediately because # without settings there is not much nagstamon can do if self.output.firstrun == True: sys.exit() else: gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def ColorsPreview(self, widget=None): """ preview for status information colors """ for state in ["ok", "warning", "critical", "unknown", "unreachable", "down", "error"]: text = self.builder.get_object("input_colorbutton_" + state + "_text").get_color().to_string() background = self.builder.get_object("input_colorbutton_" + state + "_background").get_color().to_string() label = self.builder.get_object("label_color_" + state) label.set_markup(' %s: ' %\ (text, background, state.upper())) def ColorsDefault(self, widget=None): """ reset default colors """ # text and background colors of all states get set to defaults for state in ["ok", "warning", "critical", "unknown", "unreachable", "down", "error"]: self.builder.get_object("input_colorbutton_" + state + "_text").set_color(gtk.gdk.color_parse(self.conf.__dict__["default_color_" + state + "_text"])) self.builder.get_object("input_colorbutton_" + state + "_background").set_color(gtk.gdk.color_parse(self.conf.__dict__["default_color_" + state + "_background"])) # renew preview self.ColorsPreview() def ColorsReset(self, widget=None): """ reset to previous colors """ # text and background colors of all states get set to defaults for state in ["ok", "warning", "critical", "unknown", "unreachable", "down", "error"]: self.builder.get_object("input_colorbutton_" + state + "_text").set_color(gtk.gdk.color_parse(self.conf.__dict__["color_" + state + "_text"])) self.builder.get_object("input_colorbutton_" + state + "_background").set_color(gtk.gdk.color_parse(self.conf.__dict__["color_" + state + "_background"])) # renew preview self.ColorsPreview() def DeleteServer(self, server=None, servers=None): """ delete Server after prompting """ if server: dialog = gtk.MessageDialog(parent=self.dialog, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_OK + gtk.BUTTONS_CANCEL, message_format='Really delete server "' + server + '"?') # gtk.Dialog.run() does a mini loop to wait # for some reason response is YES, not OK... but it works. if dialog.run() == gtk.RESPONSE_YES: # delete server configuration entry self.conf.servers.pop(server) # stop thread try: if self.servers[server].thread: self.servers[server].thread.Stop() except: # most probably server has been disabled and that's why there is no thread running # debug if str(self.conf.debug_mode) == "True": self.servers[server].Error(sys.exc_info()) # delete server from servers dictionary self.servers.pop(server) # fill settings dialog treeview self.FillTreeView("servers_treeview", servers, "Servers", "selected_server") # renew appearances of servers self.output.ApplyServerModifications() dialog.destroy() def DeleteAction(self, action=None, actions=None): """ delete action after prompting """ if action: dialog = gtk.MessageDialog(parent=self.dialog, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_OK + gtk.BUTTONS_CANCEL, message_format='Really delete action "' + action + '"?') # gtk.Dialog.run() does a mini loop to wait # for some reason response is YES, not OK... but it works. if dialog.run() == gtk.RESPONSE_YES: # delete actions configuration entry self.conf.actions.pop(action) # fill settings dialog treeview self.FillTreeView("actions_treeview", actions, "Actions", "selected_action") dialog.destroy() def CheckForNewVersionNow(self, widget=None): """ Check for new version of nagstamon - use connection data of configured servers """ # check if there is already a checking thread for s in self.servers.values(): if s.CheckingForNewVersion == True: # if there is already one server used for checking break break else: # start thread which checks for updates self.check = Actions.CheckForNewVersion(servers=self.servers, output=self.output, mode="normal", parent=self) self.check.start() # if one of the servers is not used to check for new version this is enough break def ToggleDebugOptions(self, widget=None): """ allow to use a file for debug output """ debug_to_file = self.builder.get_object("input_checkbutton_debug_to_file") debug_file = self.builder.get_object("input_entry_debug_file") debug_mode = self.builder.get_object("input_checkbutton_debug_mode") if debug_to_file.state == gtk.STATE_INSENSITIVE: debug_file.set_sensitive(False) if not debug_mode.get_active(): debug_to_file.hide() debug_to_file.set_sensitive(debug_mode.get_active()) debug_file.hide() debug_file.set_sensitive(debug_to_file.get_active()) else: debug_to_file.show() debug_to_file.set_sensitive(debug_mode.get_active()) debug_file.show() debug_file.set_sensitive(debug_to_file.get_active()) def ToggleNotification(self, widget=None): """ Disable notifications at all """ options = self.builder.get_object("table_notification_options") checkbutton = self.builder.get_object("input_checkbutton_notification") #options.set_sensitive(checkbutton.get_active()) if not checkbutton.get_active(): options.hide() else: options.show() def ToggleSoundOptions(self, widget=None): """ Disable notification sound when not using sound is enabled """ options = self.builder.get_object("table_notification_options_sound_options") checkbutton = self.builder.get_object("input_checkbutton_notification_sound") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) # in case custom options are shown but not selected (due to .show_all()) self.ToggleCustomSoundOptions() def ToggleCustomSoundOptions(self, widget=None): """ Disable custom notification sound """ options = self.builder.get_object("table_notification_sound_options_custom_sounds_files") checkbutton = self.builder.get_object("input_radiobutton_notification_custom_sound") if not checkbutton.get_active(): options.hide_all() else: options.show_all() def ToggleREHostOptions(self, widget=None): """ Toggle regular expression filter for hosts """ options = self.builder.get_object("hbox_re_host") checkbutton = self.builder.get_object("input_checkbutton_re_host_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleREServiceOptions(self, widget=None): """ Toggle regular expression filter for services """ options = self.builder.get_object("hbox_re_service") checkbutton = self.builder.get_object("input_checkbutton_re_service_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleREStatusInformationOptions(self, widget=None): """ Toggle regular expression filter for status """ options = self.builder.get_object("hbox_re_status_information") checkbutton = self.builder.get_object("input_checkbutton_re_status_information_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleRECriticalityOptions(self, widget=None): """ Toggle regular expression filter for criticality """ options = self.builder.get_object("hbox_re_criticality") checkbutton = self.builder.get_object("input_checkbutton_re_criticality_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() def ToggleRECriticalityFilter(self): """ 1. Always hide criticality options 2. Check if type of any enabled server is Centreon. 3. If true, show the criticality filter options """ self.builder.get_object("hbox_re_criticality").hide() self.builder.get_object("input_checkbutton_re_criticality_enabled").hide() for server in self.conf.servers: if (str(self.conf.servers[server].enabled) == "True") and (str(self.conf.servers[server].type) == "Centreon"): self.builder.get_object("input_checkbutton_re_criticality_enabled").show() def ToggleSystrayPopupOffset(self, widget=None): """ Toggle adjustment for systray-popup-offset (see sf.net bug 3389241) """ options = self.builder.get_object("hbox_systray_popup_offset") checkbutton = self.builder.get_object("input_radiobutton_icon_in_systray") #options.set_sensitive(checkbutton.get_active()) if not checkbutton.get_active(): options.hide_all() else: options.show_all() def ToggleFullscreenDisplay(self, widget=None): """ Toggle adjustment for fullscreen display choice """ options = self.builder.get_object("hbox_fullscreen_display") checkbutton = self.builder.get_object("input_radiobutton_fullscreen") #options.set_sensitive(checkbutton.get_active()) if not checkbutton.get_active(): options.hide_all() else: options.show_all() def ToggleNotificationActions(self, widget=None): """ Toggle extra notifications per level """ options = self.builder.get_object("vbox_notification_actions") checkbutton = self.builder.get_object("input_checkbutton_notification_actions") if not checkbutton.get_active(): options.hide() else: options.show() self.ToggleNotificationCustomAction() def ToggleNotificationCustomAction(self, widget=None): """ Toggle generic custom notification """ options = self.builder.get_object("table_notification_custom_action") checkbutton = self.builder.get_object("input_checkbutton_notification_custom_action") if not checkbutton.get_active(): options.hide() else: options.show() def ToggleNotificationActionWarning(self, widget=None): """ Toggle notification action for WARNING """ options = self.builder.get_object("input_entry_notification_action_warning_string") checkbutton = self.builder.get_object("input_checkbutton_notification_action_warning") if not checkbutton.get_active(): options.hide() checkbutton.set_label("WARNING") else: options.show() checkbutton.set_label("WARNING:") def ToggleNotificationActionCritical(self, widget=None): """ Toggle notification action for CRITICAL """ options = self.builder.get_object("input_entry_notification_action_critical_string") checkbutton = self.builder.get_object("input_checkbutton_notification_action_critical") if not checkbutton.get_active(): options.hide() checkbutton.set_label("CRITICAL") else: options.show() checkbutton.set_label("CRITICAL:") def ToggleNotificationActionDown(self, widget=None): """ Toggle notification action for DOWN """ options = self.builder.get_object("input_entry_notification_action_down_string") checkbutton = self.builder.get_object("input_checkbutton_notification_action_down") if not checkbutton.get_active(): options.hide() checkbutton.set_label("DOWN") else: options.show() checkbutton.set_label("DOWN:") def ToggleNotificationActionOk(self, widget=None): """ Toggle notification action for OK """ options = self.builder.get_object("input_entry_notification_action_ok_string") checkbutton = self.builder.get_object("input_checkbutton_notification_action_ok") if not checkbutton.get_active(): options.hide() checkbutton.set_label("OK") else: options.show() checkbutton.set_label("OK:") def ToggleNotificationActionsHelp(self, widget=None): """ Toggle help label for action string """ help = self.builder.get_object("label_help_notification_actions_description") help.set_visible(not help.get_visible()) def ToggleNotificationCustomActionsHelp(self, widget=None): """ Toggle help label for action string """ help = self.builder.get_object("label_help_notification_custom_actions_description") help.set_visible(not help.get_visible()) def ToggleSystemKeyring(self, widget=None): """ check on non-OSX/Windows systems if keyring and secretstorage modules are available and disable keyring checkbox if not """ checkbutton = self.builder.get_object("input_checkbutton_use_system_keyring") if not platform.system() in ["Darwin", "Windows"]: if self.conf.keyring_available: checkbutton.set_visible(True) else: checkbutton.set_visible(False) # disable keyring in general #self.conf.use_system_keyring = False # it's OK on Darwin and Windows else: checkbutton.set_visible(True) def PlaySound(self, playbutton=None): """ play sample of selected sound for Nagios Event """ try: filechooser = self.builder.get_object("input_filechooser_notification_custom_sound_" + gtk.Buildable.get_name(playbutton)) sound = Actions.PlaySound(sound="FILE", file=filechooser.get_filename(), conf=self.conf, servers=self.servers) sound.start() except Exception, err: import traceback traceback.print_exc(file=sys.stdout) class GenericServer(object): """ settings of one particular new Nagios server """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # set the gtkbuilder files self.builderfile = self.output.Resources + os.sep + "settings_server_dialog.ui" self.builder = gtk.Builder() self.builder.add_from_file(self.builderfile) self.dialog = self.builder.get_object("settings_server_dialog") # try to avoid shy dialog on MacOSX self.dialog.set_transient_for(self.settingsdialog.dialog) # assign handlers handlers_dict = { "button_ok_clicked" : self.OK, "button_cancel_clicked" : self.Cancel, "settings_dialog_close" : self.Cancel, "toggle_save_password" : self.ToggleSavePassword, "toggle_autologin_key" : self.ToggleAutoLoginKey, "toggle_proxy" : self.ToggleProxy } self.builder.connect_signals(handlers_dict) # set server type combobox to Nagios as default self.combobox = self.builder.get_object("input_combo_server_type") combomodel = gtk.ListStore(gobject.TYPE_STRING) cr = gtk.CellRendererText() self.combobox.pack_start(cr, True) self.combobox.set_attributes(cr, text=0) for server in Actions.get_registered_server_type_list(): combomodel.append((server,)) self.combobox.set_model(combomodel) self.combobox.set_active(0) self.combobox.connect('changed', self.on_server_type_change) # initialize server type dependent dialog outfit self.on_server_type_change(self.combobox) # set specific defaults or server settings self.initialize() def show(self): # show filled settings dialog and wait thanks to gtk.run() self.dialog.run() gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def initialize(self): """ set server settings to default values """ if self.server == "": # new server # enable server by default self.builder.get_object("input_checkbutton_enabled").set_active(True) # disable autologin by default self.ToggleAutoLoginKey() # save password by default self.builder.get_object("input_checkbutton_save_password").set_active(True) # disable proxy by default self.builder.get_object("input_checkbutton_use_proxy").set_active(False) # set first monitor type as default self.combobox.set_active(0) # default monitor server addresses self.builder.get_object("input_entry_monitor_url").set_text("https://monitor-server") self.builder.get_object("input_entry_monitor_cgi_url").set_text("https://monitor-server/monitor/cgi-bin") # default user and password self.builder.get_object("input_entry_username").set_text("user") self.builder.get_object("input_entry_password").set_text("password") # default proxy settings self.builder.get_object("input_entry_proxy_address").set_text("http://proxy:port/") self.builder.get_object("input_entry_proxy_username").set_text("proxyuser") self.builder.get_object("input_entry_proxy_password").set_text("proxypassword") else: # edit or copy a server keys = self.conf.servers[self.server].__dict__.keys() # walk through all relevant input types to fill dialog with existing settings for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_", "input_spinbutton_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to fill it with desired content # so we try them all, one of them should work try: j.set_text(self.conf.servers[self.server].__dict__[key]) except: pass try: if str(self.conf.servers[self.server].__dict__[key]) == "True": j.set_active(True) if str(self.conf.servers[self.server].__dict__[key]) == "False": j.set_active(False) except: pass try: j.set_value(int(self.conf.servers[self.server].__dict__[key])) except: pass # set server type combobox which cannot be set by above hazard method servers = Actions.get_registered_server_type_list() server_types = dict([(x[1], x[0]) for x in enumerate(servers)]) # set server type self.combobox.set_active(server_types[self.conf.servers[self.server].type]) # show password - or not #self.ToggleSavePassword() # show settings options for proxy - or not self.ToggleProxy() def on_server_type_change(self, combobox): """ Disables controls as it is set in server class from former ServerDialogHelper class which contained common logic for server dialog might be of interest in case server type is changed and dialog content should be adjusted to reflect different labels/entry fields """ active = combobox.get_active_iter() model = combobox.get_model() if not model: return server = Actions.get_registered_servers()[model.get_value(active, 0)] # make everything visible for item_id in ["label_monitor_cgi_url", "input_entry_monitor_cgi_url", "input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"]: item = self.builder.get_object(item_id) if item is not None: item.set_visible(True) # so we can hide what may be hidden if len(server.DISABLED_CONTROLS) != 0: for item_id in server.DISABLED_CONTROLS: item = self.builder.get_object(item_id) if item is not None: item.set_visible(False) def OK(self, widget): """ New server configured """ # put changed data into new server, which will get into the servers dictionary after the old # one has been deleted new_server = Config.Server() keys = new_server.__dict__.keys() for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_", "input_spinbutton_", "input_filechooser_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to get its content # so we try them all, one of them should work try: new_server.__dict__[key] = j.get_text() except: pass try: new_server.__dict__[key] = j.get_active() except: pass try: new_server.__dict__[key] = int(j.get_value()) except: pass # set server type combobox which cannot be set by above hazard method combobox = self.builder.get_object("input_combo_server_type") active = combobox.get_active_iter() model = combobox.get_model() new_server.__dict__["type"] = model.get_value(active, 0) # workaround for cgi-url not needed by certain monitor types server = Actions.get_registered_servers()[new_server.type] if "input_entry_monitor_cgi_url" in server.DISABLED_CONTROLS: new_server.monitor_cgi_url = new_server.monitor_url # URLs should not end with / - clean it new_server.monitor_url = new_server.monitor_url.rstrip("/") new_server.monitor_cgi_url = new_server.monitor_cgi_url.rstrip("/") # check if there is already a server named like the new one if new_server.name in self.conf.servers: self.output.Dialog(message='A server named "' + new_server.name + '" already exists.') else: # put in new one self.conf.servers[new_server.name] = new_server # create new server thread created_server = Actions.CreateServer(new_server, self.conf, self.output.debug_queue, self.output.Resources) if created_server is not None: self.servers[new_server.name] = created_server if str(self.conf.servers[new_server.name].enabled) == "True": # start new thread (should go to Actions!) self.servers[new_server.name].thread = Actions.RefreshLoopOneServer(server=self.servers[new_server.name], output=self.output, conf=self.conf) self.servers[new_server.name].thread.start() # fill settings dialog treeview self.settingsdialog.FillTreeView("servers_treeview", self.conf.servers, "Servers", "selected_server") # care about Centreon criticality filter self.settingsdialog.ToggleRECriticalityFilter() # apply settings for modified servers self.output.ApplyServerModifications() # destroy new server dialog gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def Cancel(self, widget): """ settings dialog got cancelled """ if not self.conf.unconfigured == True: gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() else: sys.exit() def ToggleSavePassword(self, widget=None): """ Disable password input box """ checkbutton = self.builder.get_object("input_checkbutton_save_password") is_active = checkbutton.get_active() item = self.builder.get_object("label_password") item.set_sensitive(is_active) item = self.builder.get_object("input_entry_password") item.set_sensitive(is_active) ###if not is_active: ### item.set_text("") def ToggleAutoLoginKey(self, widget=None): """ Disable autologin key input box """ use_autologin = self.builder.get_object("input_checkbutton_use_autologin") is_active = use_autologin.get_active() item = self.builder.get_object("label_autologin_key") item.set_sensitive( is_active ) item = self.builder.get_object("input_entry_autologin_key") item.set_sensitive( is_active ) ###if not is_active: ### item.set_text("") #disable save password item = self.builder.get_object("input_checkbutton_save_password") item.set_active( False ) item.set_sensitive( not is_active ) item = self.builder.get_object("label_password") item.set_sensitive( not is_active ) item = self.builder.get_object("input_entry_password") item.set_sensitive( not is_active ) item.set_text("") def ToggleProxy(self, widget=None): """ Disable proxy options """ checkbutton = self.builder.get_object("input_checkbutton_use_proxy") self.ToggleProxyFromOS(checkbutton.get_active()) self.ToggleProxyAddress(checkbutton.get_active()) def ToggleProxyFromOS(self, widget=None): """ toggle proxy from OS when using proxy is enabled """ checkbutton = self.builder.get_object("input_checkbutton_use_proxy_from_os") #checkbutton.set_sensitive(self.builder.get_object("input_checkbutton_use_proxy").get_active()) if self.builder.get_object("input_checkbutton_use_proxy").get_active(): self.builder.get_object("input_checkbutton_use_proxy_from_os").show() else: self.builder.get_object("input_checkbutton_use_proxy_from_os").hide() def ToggleProxyAddress(self, widget=None): """ toggle proxy address options when not using proxy is enabled """ use_proxy = self.builder.get_object("input_checkbutton_use_proxy") use_proxy_from_os = self.builder.get_object("input_checkbutton_use_proxy_from_os") # depending on checkbox state address fields wil be active if use_proxy.get_active() == True: # always the opposite of os proxy selection state = not use_proxy_from_os.get_active() else: state = False for n in ("label_proxy_address", "input_entry_proxy_address", "label_proxy_username", "input_entry_proxy_username", "label_proxy_password", "input_entry_proxy_password"): item = self.builder.get_object(n) if state: item.show() else: item.hide() class NewServer(GenericServer): """ settings of one particuliar new Nagios server """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # create new dummy server self.server = "" GenericServer.__init__(self, **kwds) # set title of settings dialog self.dialog.set_title("New server") class EditServer(GenericServer): """ settings of one particuliar Nagios server """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] GenericServer.__init__(self, **kwds) # in case server has been selected do nothing if not self.server == None: # set title of settings dialog self.dialog.set_title("Edit server " + self.server) keys = self.conf.servers[self.server].__dict__.keys() # walk through all relevant input types to fill dialog with existing settings for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_", "input_spinbutton_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to fill it with desired content # so we try them all, one of them should work try: j.set_text(self.conf.servers[self.server].__dict__[key]) except: pass try: if str(self.conf.servers[self.server].__dict__[key]) == "True": j.set_active(True) if str(self.conf.servers[self.server].__dict__[key]) == "False": j.set_active(False) except: pass try: j.set_value(int(self.conf.servers[self.server].__dict__[key])) except: pass # set server type combobox which cannot be set by above hazard method servers = Actions.get_registered_server_type_list() server_types = dict([(x[1], x[0]) for x in enumerate(servers)]) self.combobox.set_active(server_types[self.conf.servers[self.server].type]) def initialize(self): """ fill dialog with server settings """ GenericServer.initialize(self) # set title of settings dialog self.dialog.set_title("Edit server " + self.server) def OK(self, widget): """ settings dialog got OK-ed """ # put changed data into new server, which will get into the servers dictionary after the old # one has been deleted new_server = Config.Server() keys = new_server.__dict__.keys() for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_", "input_spinbutton_", "input_filechooser_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to get its content # so we try them all, one of them should work try: new_server.__dict__[key] = j.get_text() except: pass try: new_server.__dict__[key] = j.get_active() except: pass try: new_server.__dict__[key] = int(j.get_value()) except: pass # set server type combobox which cannot be set by above hazard method combobox = self.builder.get_object("input_combo_server_type") active = combobox.get_active_iter() model = combobox.get_model() new_server.__dict__["type"] = model.get_value(active, 0) # workaround for cgi-url not needed by certain monitor types server = Actions.get_registered_servers()[new_server.type] if "input_entry_monitor_cgi_url" in server.DISABLED_CONTROLS: new_server.monitor_cgi_url = new_server.monitor_url # URLs should not end with / - clean it new_server.monitor_url = new_server.monitor_url.rstrip("/") new_server.monitor_cgi_url = new_server.monitor_cgi_url.rstrip("/") # check if there is already a server named like the new one if new_server.name in self.conf.servers and new_server.name != self.server: self.output.Dialog(message="A server named " + new_server.name + " already exists.") else: # delete old server configuration entry self.conf.servers.pop(self.server) try: # stop thread - only if it is yet initialized as such if self.servers[self.server].thread: self.servers[self.server].thread.Stop() except: import traceback traceback.print_exc(file=sys.stdout) # delete server from servers dictionary self.servers.pop(self.server) # put in new one self.conf.servers[new_server.name] = new_server # create new server thread created_server = Actions.CreateServer(new_server, self.conf, self.output.debug_queue, self.output.Resources) if created_server is not None: self.servers[new_server.name] = created_server if str(self.conf.servers[new_server.name].enabled) == "True": # start new thread (should go to Actions) self.servers[new_server.name].thread = Actions.RefreshLoopOneServer(server=self.servers[new_server.name], output=self.output, conf=self.conf) self.servers[new_server.name].thread.start() # fill settings dialog treeview self.settingsdialog.FillTreeView("servers_treeview", self.conf.servers, "Servers", "selected_server") # care about Centreon criticality filter self.settingsdialog.ToggleRECriticalityFilter() # apply settings for modified servers self.output.ApplyServerModifications() # hide dialog gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def Cancel(self, widget): """ settings dialog got cancelled """ gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() class CopyServer(GenericServer): """ copy a server """ def initialize(self): # get existing properties from action like it was edited GenericServer.initialize(self) # set title of settings dialog self.dialog.set_title("Copy server " + self.server) # modify name if action to indicate copy self.entry_name = self.builder.get_object("input_entry_name") self.entry_name.set_text("Copy of %s" % (self.entry_name.get_text())) class GenericAction(object): """ settings of one particuliar action """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # set the gtkbuilder files self.builderfile = self.output.Resources + os.sep + "settings_action_dialog.ui" self.builder = gtk.Builder() self.builder.add_from_file(self.builderfile) self.dialog = self.builder.get_object("settings_action_dialog") # try to avoid shy dialog on MacOSX self.dialog.set_transient_for(self.settingsdialog.dialog) # assign handlers handlers_dict = { "button_ok_clicked" : self.OK, "button_cancel_clicked" : self.Cancel, "settings_dialog_close" : self.Cancel, "checkbutton_re_host_enabled": self.ToggleREHostOptions, "checkbutton_re_service_enabled": self.ToggleREServiceOptions, "checkbutton_re_status_information_enabled": self.ToggleREStatusInformationOptions, "checkbutton_re_criticality_enabled": self.ToggleRECriticalityOptions, "button_help_string_clicked": self.ToggleActionStringHelp, "button_help_type_clicked": self.ToggleActionTypeHelp, } self.builder.connect_signals(handlers_dict) # fill combobox for action type with options self.combobox_action_type = self.builder.get_object("input_combo_action_type") self.combomodel_action_type = gtk.ListStore(gobject.TYPE_STRING) cr = gtk.CellRendererText() self.combobox_action_type.pack_start(cr, True) self.combobox_action_type.set_attributes(cr, text=0) for action_type in ["Browser", "Command", "URL"]: self.combomodel_action_type.append((action_type,)) self.combobox_action_type.set_model(self.combomodel_action_type) # fill combobox for monitor type with options self.combobox_monitor_type = self.builder.get_object("input_combo_monitor_type") self.combomodel_monitor_type = gtk.ListStore(gobject.TYPE_STRING) cr = gtk.CellRendererText() self.combobox_monitor_type.pack_start(cr, True) self.combobox_monitor_type.set_attributes(cr, text=0) self.monitor_types = sorted(Actions.get_registered_server_type_list()) # as default setting - would be "" in config file self.monitor_types.insert(0, "All monitor servers") # transform monitor types list to a dictionary with numbered values to handle combobox index later self.monitor_types = dict(zip(self.monitor_types, range(len(self.monitor_types)))) for monitor_type in sorted(self.monitor_types.keys()): self.combomodel_monitor_type.append((monitor_type,)) self.combobox_monitor_type.set_model(self.combomodel_monitor_type) # if action applies to all monitors which is "" as default in config file its index is like "All monitor servers" self.monitor_types[""] = 0 self.initialize() def show(self): # show filled settings dialog and wait thanks to gtk.run() self.dialog.run() gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def initialize(self): """ set defaults for action """ # if uninitialized action (e.g. new one) is used don't access actions dictionary if self.action == "": # ...but use a dummy object with default settings action = Config.Action() # enable action by default self.builder.get_object("input_checkbutton_enabled").set_active(True) # action type combobox should be set to default self.combobox_action_type = self.builder.get_object("input_combo_action_type") self.combobox_action_type.set_active(0) # monitor type combobox should be set to default self.combobox_monitor_type = self.builder.get_object("input_combo_monitor_type") self.combobox_monitor_type.set_active(0) else: action = self.conf.actions[self.action] # adjust combobox to used action type self.combobox_action_type = self.builder.get_object("input_combo_action_type") self.combobox_action_type.set_active({"browser":0, "command":1, "url":2}[self.conf.actions[self.action].type]) self.combobox_action_type = self.builder.get_object("input_combo_monitor_type") self.combobox_action_type.set_active(self.monitor_types[self.conf.actions[self.action].monitor_type]) keys = action.__dict__.keys() # walk through all relevant input types to fill dialog with existing settings for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to fill it with desired content # so we try them all, one of them should work try: j.set_text(action.__dict__[key]) except: pass try: if str(action.__dict__[key]) == "True": j.set_active(True) if str(action.__dict__[key]) == "False": j.set_active(False) except: pass try: j.set_value(int(self.conf.__dict__[key])) except: pass # disable help per default self.builder.get_object("label_help_string_description").set_visible(False) self.builder.get_object("label_help_type_description").set_visible(False) # toggle some GUI elements self.ToggleREHostOptions() self.ToggleREServiceOptions() self.ToggleREStatusInformationOptions() self.ToggleRECriticalityOptions() def OK(self, widget): """ New action configured pr existing one edited """ # put changed data into new server, which will get into the servers dictionary after the old # one has been deleted new_action = Config.Action() keys = new_action.__dict__.keys() for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to get its content # so we try them all, one of them should work try: new_action.__dict__[key] = j.get_text() except: try: new_action.__dict__[key] = j.get_active() except: try: new_action.__dict__[key] = int(j.get_value()) except: pass # set action type combobox which cannot be set by above hazard method self.combobox_action_type = self.builder.get_object("input_combo_action_type") active = self.combobox_action_type.get_active_iter() self.combomodel_action_type = self.combobox_action_type.get_model() new_action.type = self.combomodel_action_type .get_value(active, 0).lower() # set monitor type combobox which cannot be set by above hazard method self.combobox_monitor_type = self.builder.get_object("input_combo_monitor_type") active = self.combobox_monitor_type.get_active_iter() self.combomodel_monitor_type = self.combobox_monitor_type.get_model() new_action.monitor_type = self.combomodel_monitor_type.get_value(active, 0) # if action applies to all monitor types its monitor_type should be "" # because it is "All monitor servers" in Combobox if not new_action.monitor_type in Actions.get_registered_server_type_list(): new_action.monitor_type = "" # check if there is already an action named like the new one if new_action.name in self.conf.actions: self.output.Dialog(message='An action named "' + new_action.name + '" already exists.') else: # put in new one self.conf.actions[new_action.name] = new_action # fill settings dialog treeview self.settingsdialog.FillTreeView("actions_treeview", self.conf.actions, "Actions", "selected_action") # destroy new action dialog gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def Cancel(self, widget): """ settings dialog got cancelled """ gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() def ToggleREHostOptions(self, widget=None): """ Toggle regular expression filter for hosts """ options = self.builder.get_object("hbox_re_host") checkbutton = self.builder.get_object("input_checkbutton_re_host_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleREServiceOptions(self, widget=None): """ Toggle regular expression filter for services """ options = self.builder.get_object("hbox_re_service") checkbutton = self.builder.get_object("input_checkbutton_re_service_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleREStatusInformationOptions(self, widget=None): """ Toggle regular expression filter for status """ options = self.builder.get_object("hbox_re_status_information") checkbutton = self.builder.get_object("input_checkbutton_re_status_information_enabled") if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleRECriticalityOptions(self, widget=None): """ Toggle regular expression filter for criticality """ options = self.builder.get_object("hbox_re_criticality") checkbutton = self.builder.get_object("input_checkbutton_re_criticality_enabled") if not checkbutton == None: if not checkbutton.get_active(): options.hide_all() else: options.show_all() options.set_sensitive(checkbutton.get_active()) def ToggleActionStringHelp(self, widget=None): """ Toggle help label for action string """ help = self.builder.get_object("label_help_string_description") help.set_visible(not help.get_visible()) def ToggleActionTypeHelp(self, widget=None): """ Toggle help label for action type """ help = self.builder.get_object("label_help_type_description") help.set_visible(not help.get_visible()) class NewAction(GenericAction): """ generic settings of one particuliar new action server """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] # create new dummy action self.action = "" GenericAction.__init__(self, **kwds) # set title of settings dialog self.dialog.set_title("New action") class EditAction(GenericAction): """ generic settings of one particuliar new action server """ def initialize(self): """ extra initialization needed for every call """ GenericAction.initialize(self) # set title of settings dialog self.dialog.set_title("Edit action " + self.action) def OK(self, widget): """ New action configured pr existing one edited """ # put changed data into new server, which will get into the servers dictionary after the old # one has been deleted new_action = Config.Action() keys = new_action.__dict__.keys() for i in ["input_entry_", "input_checkbutton_", "input_radiobutton_"]: for key in keys: j = self.builder.get_object(i + key) if not j: continue # some hazard, every widget has other methods to get its content # so we try them all, one of them should work try: new_action.__dict__[key] = j.get_text() except: try: new_action.__dict__[key] = j.get_active() except: try: new_action.__dict__[key] = int(j.get_value()) except: pass # set server type combobox which cannot be set by above hazard method self.combobox_action_type = self.builder.get_object("input_combo_action_type") active = self.combobox_action_type.get_active_iter() model = self.combobox_action_type.get_model() new_action.type = model.get_value(active, 0).lower() # set monitor type combobox which cannot be set by above hazard method self.combobox_monitor_type = self.builder.get_object("input_combo_monitor_type") active = self.combobox_monitor_type.get_active_iter() self.combomodel_monitor_type = self.combobox_monitor_type.get_model() new_action.monitor_type = self.combomodel_monitor_type.get_value(active, 0) # if action applies to all monitor types its monitor_type should be "" # because it is "All monitor servers" in Combobox if not new_action.monitor_type in Actions.get_registered_server_type_list(): new_action.monitor_type = "" # check if there is already an action named like the new one if new_action.name in self.conf.actions and new_action.name != self.action: self.output.Dialog(message='An action named "' + new_action.name + '" already exists.') else: # delete old one self.conf.actions.pop(self.action) # put in new one self.conf.actions[new_action.name] = new_action # fill settings dialog treeview self.settingsdialog.FillTreeView("actions_treeview", self.conf.actions, "Actions", "selected_action") # destroy new action dialog gobject.idle_add(self.output.DeleteGUILock, str(self.__class__.__name__)) self.dialog.hide() class CopyAction(GenericAction): """ copies an existing action """ def initialize(self): """ extra initialization needed for every call """ # get existing properties from action like it was edited GenericAction.initialize(self) # set title of settings dialog self.dialog.set_title("Copy action " + self.action) # modify name if action to indicate copy self.entry_name = self.builder.get_object("input_entry_name") self.entry_name.set_text("Copy of %s" % (self.entry_name.get_text())) class AuthenticationDialog: """ used in case password should not be stored "server" is here a Config.Server() instance given from nagstamon.py at startup, not a GenericServer()! """ def __init__(self, **kwds): # the usual... for k in kwds: self.__dict__[k] = kwds[k] # set the gtkbuilder files self.builderfile = self.Resources + os.sep + "authentication_dialog.ui" self.builder = gtk.Builder() self.builder.add_from_file(self.builderfile) self.dialog = self.builder.get_object("authentication_dialog") # assign handlers handlers_dict = { "button_ok_clicked" : self.OK, "button_exit_clicked" : self.Exit, "toggle_autologin_key_auth" : self.ToggleAutoLoginKeyAuth, "button_disable_clicked" : self.Disable } self.builder.connect_signals(handlers_dict) self.label_monitor = self.builder.get_object("label_monitor") self.entry_username = self.builder.get_object("input_entry_username") self.entry_password = self.builder.get_object("input_entry_password") self.entry_autologin_key = self.builder.get_object("input_entry_autologin_key") self.dialog.set_title("Nagstamon authentication for " + self.server.name) self.label_monitor.set_text("Please give the correct credentials for "+ self.server.name + ":") self.entry_username.set_text(str(self.server.username)) self.entry_password.set_text(str(self.server.password)) self.entry_autologin_key.set_text(str(self.server.autologin_key)) self.ToggleAutoLoginKeyAuth() # omitting .show_all() leads to crash under Linux - why? self.dialog.show_all() # any monitor that is not Centreon does not need autologin entry if not self.server.type == "Centreon": self.entry_autologin_key.set_visible(False) self.builder.get_object("input_checkbutton_use_autologin").set_visible(False) self.builder.get_object("label_autologin_key").set_visible(False) self.builder.get_object("input_entry_autologin_key").set_visible(False) self.dialog.run() self.dialog.destroy() def OK(self, widget): self.server.username = self.entry_username.get_text() self.server.password = self.entry_password.get_text() self.server.autologin_key = self.entry_autologin_key.get_text() toggle_save_password = self.builder.get_object("input_checkbutton_save_password") toggle_use_autologin = self.builder.get_object("input_checkbutton_use_autologin") if toggle_save_password.get_active() == True: # store authentication information in config self.conf.servers[self.server.name].username = self.server.username self.conf.servers[self.server.name].password = self.server.password self.conf.servers[self.server.name].save_password = True self.conf.SaveConfig() if toggle_use_autologin.get_active() == True: # store autologin information in config self.conf.servers[self.server.name].username = self.server.username self.conf.servers[self.server.name].password = "" self.conf.servers[self.server.name].save_password = False self.conf.servers[self.server.name].autologin_key = self.server.autologin_key self.conf.servers[self.server.name].use_autologin = True self.conf.SaveConfig() def Disable(self, widget): # the old settings self.conf.servers[self.server.name].enabled = False def Exit(self, widget): sys.exit() def ToggleAutoLoginKeyAuth(self, widget=None): """ Disable autologin key input box """ use_autologin = self.builder.get_object("input_checkbutton_use_autologin") is_active = use_autologin.get_active() item = self.builder.get_object("label_autologin_key") item.set_sensitive( is_active ) item = self.builder.get_object("input_entry_autologin_key") item.set_sensitive( is_active ) if not is_active: item.set_text("") #disable save password item = self.builder.get_object("input_checkbutton_save_password") item.set_active( False ) item.set_sensitive( not is_active ) item = self.builder.get_object("label_password") item.set_sensitive( not is_active ) item = self.builder.get_object("input_entry_password") item.set_sensitive( not is_active ) item.set_text("") class DummyStatusIcon(object): """ trayicon for MacOSX - only purpose is not showing trayicon because making it work as on Windows or Linux seems to need too much efford """ def __init__(self): pass def set_from_file(self, *args, **kwds): pass def set_visible(self, *args, **kwds): pass def get_geometry(self, *args, **kwds): pass def connect(self, *args, **kwds): pass def set_from_pixbuf(self, *args, **kwds): pass def set_blinking(self, *args, **kwds): pass class ButtonWithIcon(gtk.Button): """ Button with an icon - reduces code """ def __init__(self, **kwds): # add all keywords to object for k in kwds: self.__dict__[k] = kwds[k] gtk.Button.__init__(self) # HBox is necessary because gtk.Button allows only one child self.HBox = gtk.HBox() self.Icon = gtk.Image() self.Icon.set_from_file(self.output.Resources + os.sep + self.icon) self.HBox.add(self.Icon) if self.label != "": self.Label = gtk.Label(" " + self.label) self.HBox.add(self.Label) self.set_relief(gtk.RELIEF_NONE) self.add(self.HBox) def show(self): """ 'normal' .show() does not show HBox and Icon """ gtk.Button.show(self) self.HBox.show() self.Icon.show() if self.__dict__.has_key("Label"): self.Label.show() def hide(self): """ 'normal' .hide() does not hide HBox and Icon """ gtk.Button.hide(self) self.HBox.hide() self.Icon.hide() if self.__dict__.has_key("Label"): self.Label.hide() Nagstamon/Nagstamon/Objects.py000066400000000000000000000154621240775040100167070ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import Actions class Column(object): ATTR_NAME = 'name' DEFAULT_VALUE = '' SORT_FUNCTION_NAME = 'sort_function' def __init__(self, row): self.value = self._get_value(row) def __str__(self): return str(self.value) def _get_value(self, row): if hasattr(row, self.ATTR_NAME): return getattr(row, self.ATTR_NAME) return self.DEFAULT_VALUE @classmethod def get_label(cls): """ Table header column label """ return ' '.join([x.capitalize() for x in cls.ATTR_NAME.split('_')]) @classmethod def has_customized_sorting(cls): return hasattr(cls, cls.SORT_FUNCTION_NAME) class CustomSortingColumn(Column): CHOICES = [] # list of expected values with expected order @classmethod def sort_function(cls, model, iter1, iter2, column): """ Overrides default sorting behaviour """ data1, data2 = [model.get_value(x, column) for x in (iter1, iter2)] # this happens since liststore (aka tab_model) is an attribute of server and not created every time # new, so sometimes data2 is simply "None" if data2 == None: return cls.CHOICES.index(data1) try: return cls.CHOICES.index(data1) - cls.CHOICES.index(data2) except ValueError, err: # value not in CHOICES try: return cmp(cls.CHOICES.index(data1), cls.CHOICES.index(data2)) except ValueError, err: try: return cls.CHOICES.index(data1) except: return cls.CHOICES.index(data2) class StatusColumn(CustomSortingColumn): ATTR_NAME = 'status' CHOICES = ['WARNING', 'UNKNOWN', 'CRITICAL', 'UNREACHABLE', 'DOWN'] class HostColumn(Column): ATTR_NAME = 'host' def _get_value(self, row): return row.get_host_name() class ServiceColumn(Column): def _get_value(self, row): return row.get_service_name() @classmethod def get_label(cls): return 'Service' class LastCheckColumn(Column): ATTR_NAME = 'last_check' class DurationColumn(CustomSortingColumn): ATTR_NAME = 'duration' @classmethod def sort_function(cls, model, iter1, iter2, column): """ Overrides default sorting behaviour """ data1, data2 = [model.get_value(x, column) for x in (iter1, iter2)] try: first = Actions.MachineSortableDate(data1) second = Actions.MachineSortableDate(data2) except ValueError, err: print err return cmp(first, second) return first - second class AttemptColumn(Column): ATTR_NAME = 'attempt' class StatusInformationColumn(Column): ATTR_NAME = 'status_information' class GenericObject(object): """ template for hosts and services """ def __init__(self): self.name = "" self.status = "" self.status_information = "" # default state is soft, to be changed by to-be-written status_type check self.status_type = "" self.last_check = "" self.duration = "" self.attempt = "" self.passiveonly = False self.acknowledged = False self.notifications_disabled = False self.flapping = False self.scheduled_downtime = False self.visible = True # Check_MK also has site info self.site = "" # server to be added to hash self.server = "" def is_passive_only(self): return bool(self.passiveonly) def is_flapping(self): return bool(self.flapping) def has_notifications_disabled(self): return bool(self.notifications) def is_acknowledged(self): return bool(self.acknowledged) def is_in_scheduled_downtime(self): return bool(self.scheduled_downtime) def is_visible(self): return bool(self.visible) def get_name(self): """ return stringified name """ return str(self.name) def get_host_name(self): """ Extracts host name from status item. Presentation purpose. """ return '' def get_service_name(self): """ Extracts service name from status item. Presentation purpose. """ return '' def get_hash(self): """ returns hash of event status information - different for host and service thus empty here """ return '' class GenericHost(GenericObject): """ one host which is monitored by a Nagios server, gets populated with services """ def __init__(self): GenericObject.__init__(self) # take all the faulty services on host self.services = dict() def get_host_name(self): return str(self.name) def is_host(self): """ decides where to put acknowledged/downtime pixbufs in Liststore for Treeview in Popwin """ return True def get_hash(self): """ return hash for event history tracking """ return " ".join((self.server, self.site, self.name, self.status)) class GenericService(GenericObject): """ one service which runs on a host """ def __init__(self): GenericObject.__init__(self) def get_host_name(self): return str(self.host) def get_service_name(self): return str(self.name) def is_host(self): """ decides where to put acknowledged/downtime pixbufs in Liststore for Treeview in Popwin """ return False def get_hash(self): """ return hash for event history tracking """ return " ".join((self.server, self.site, self.host, self.name, self.status)) class Result(object): """ multi purpose result object, used in Servers.Generic.FetchURL() """ result = "" error = "" def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] Nagstamon/Nagstamon/Server/000077500000000000000000000000001240775040100162025ustar00rootroot00000000000000Nagstamon/Nagstamon/Server/Centreon.py000066400000000000000000000723031240775040100203360ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import urllib, urllib2 import webbrowser import socket import sys import re import copy from Nagstamon.Objects import * from Nagstamon.Server.Generic import GenericServer class CentreonServer(GenericServer): TYPE = 'Centreon' # centreon generic web interface uses a sid which is needed to ask for news SID = None # count for SID regeneration SIDcount = 0 # URLs for browser shortlinks/buttons on popup window BROWSER_URLS= { "monitor": "$MONITOR$/main.php?p=1",\ "hosts": "$MONITOR$/main.php?p=20103&o=hpb",\ "services": "$MONITOR$/main.php?p=20202&o=svcpb",\ "history": "$MONITOR$/main.php?p=203"} # A Monitor CGI URL is not necessary so hide it in settings DISABLED_CONTROLS = ["label_monitor_cgi_url", "input_entry_monitor_cgi_url", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] # newer Centreon versions (2.3+?) have different URL paths with a "/ndo" fragment # will be checked by _get_ndo_url() but default is /xml/ndo/ # new in Centreon 2.4 seems to be a /xml/broker/ URL so this will be tried first XML_NDO = "xml/broker" # HARD/SOFT state mapping HARD_SOFT = {"(H)": "hard", "(S)": "soft"} # apparently necessesary because of non-english states as in https://github.com/HenriWahl/Nagstamon/issues/91 TRANSLATIONS = {"INDISPONIBLE": "DOWN", "INJOIGNABLE": "UNREACHABLE", "CRITIQUE": "CRITICAL", "INCONNU": "UNKNOWN", "ALERTE": "WARNING"} def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] GenericServer.__init__(self, **kwds) # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Downtime"] def init_HTTP(self): """ initialize HTTP connection """ if self.HTTPheaders == {}: GenericServer.init_HTTP(self) # Centreon xml giveback method just should exist self.HTTPheaders["xml"] = {} def reset_HTTP(self): """ Centreon needs deletion of SID """ self.HTTPheaders = {} self.SID = None self.SIDcount = 0 self._get_sid() def init_config(self): """ dummy init_config, called at thread start, not really needed here, just omit extra properties """ pass def open_tree_view(self, host, service=""): if str(self.use_autologin) == "True": auth = "&autologin=1&useralias=" + self.username + "&token=" + self.autologin_key if service == "": webbrowser.open(self.monitor_cgi_url + "/index.php?" + urllib.urlencode({"p":201,"o":"hd", "host_name":host}) + auth ) else: webbrowser.open(self.monitor_cgi_url + "/index.php?" + urllib.urlencode({"p":202, "o":"svcd", "host_name":host, "service_description":service}) + auth ) else: # must be a host if service is empty... if service == "": webbrowser.open(self.monitor_cgi_url + "/main.php?" + urllib.urlencode({"p":201,"o":"hd", "host_name":host})) else: webbrowser.open(self.monitor_cgi_url + "/main.php?" + urllib.urlencode({"p":202, "o":"svcd", "host_name":host, "service_description":service})) def get_start_end(self, host): """ get start and end time for downtime from Centreon server """ try: cgi_data = urllib.urlencode({"p":"20106",\ "o":"ah",\ "host_name":host}) result = self.FetchURL(self.monitor_cgi_url + "/main.php?" + cgi_data, giveback="obj") html, error = result.result, result.error if error == "": html = result.result start_time = html.find(attrs={"name":"start"}).attrMap["value"] end_time = html.find(attrs={"name":"end"}).attrMap["value"] # give values back as tuple return start_time, end_time except: self.Error(sys.exc_info()) return "n/a", "n/a" def GetHost(self, host): """ Centreonified way to get host ip - attribute "a" in down hosts xml is of no use for up hosts so we need to get ip anyway from web page """ # the fastest method is taking hostname as used in monitor if str(self.conf.connect_by_host) == "True" or host == "": return Result(result=host) # do a web interface search limited to only one result - the hostname cgi_data = urllib.urlencode({"sid":self.SID,\ "search":host,\ "num":0,\ "limit":1,\ "sort_type":"hostname",\ "order":"ASC",\ "date_time_format_status":"d/m/Y H:i:s",\ "o":"h",\ "p":20102,\ "time":0}) result = self.FetchURL(self.monitor_cgi_url + "/include/monitoring/status/Hosts/" + self.XML_NDO + "/hostXML.php?"\ + cgi_data, giveback="xml") xmlobj = result.result if len(xmlobj) != 0: # when connection by DNS is not configured do it by IP try: if str(self.conf.connect_by_dns) == "True": # try to get DNS name for ip, if not available use ip try: address = socket.gethostbyaddr(xmlobj.l.a.text)[0] del xmlobj except: self.Error(sys.exc_info()) address = str(xmlobj.l.a.text) del xmlobj else: address = str(xmlobj.l.a.text) del xmlobj except: result, error = self.Error(sys.exc_info()) return Result(error=error) else: result, error = self.Error(sys.exc_info()) return Result(error=error) # print IP in debug mode if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug ="IP of %s:" % (host) + " " + address) # give back host or ip return Result(result=address) def _get_sid(self): """ gets a shiny new SID for XML HTTP requests to Centreon cutting it out via .partition() from raw HTML additionally get php session cookie """ # BROWSER_URLS using autologin if str(self.use_autologin) == "True": auth = "&autologin=1&useralias=" + self.username + "&token=" + self.autologin_key self.BROWSER_URLS= { "monitor": "$MONITOR$/index.php?p=1" + auth,\ "hosts": "$MONITOR$/index.php?p=20103&o=hpb" + auth,\ "services": "$MONITOR$/index.php?p=20202&o=svcpb" + auth,\ "history": "$MONITOR$/index.php?p=203" + auth} try: if str(self.use_autologin) == "True": raw = self.FetchURL(self.monitor_cgi_url + "/index.php?p=101&autologin=1&useralias=" + self.username + "&token=" + self.autologin_key, giveback="raw") #p=101&autologin=1&useralias=foscarini&token=8sEvwyEcMt else: login_data = urllib.urlencode({"useralias" : self.username, "password" : self.password, "submit" : "Login"}) raw = self.FetchURL(self.monitor_cgi_url + "/index.php",cgi_data=login_data, giveback="raw") del raw sid = str(self.Cookie._cookies.values()[0].values()[0]["PHPSESSID"].value) return Result(result=sid) except: result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) def _get_ndo_url(self): """ Find out where this instance of Centreon is publishing the status XMLs Centreon + ndo - /include/monitoring/status/Hosts/xml/hostXML.php Centreon + broker 2.3/2.4 - /include/monitoring/status/Hosts/xml/{ndo,broker}/hostXML.php according to configuration regexping HTML for Javascript """ cgi_data = urllib.urlencode({"p":201}) result = self.FetchURL(self.monitor_cgi_url + "/main.php?" + cgi_data, cgi_data=urllib.urlencode({"sid":self.SID}), giveback="raw") raw, error = result.result, result.error if error == "": if re.search("var _addrXML.*xml\/host", raw): self.XML_NDO = "xml" elif re.search("var _addrXML.*xml\/ndo\/host", raw): self.XML_NDO = "xml/ndo" elif re.search("var _addrXML.*xml\/broker\/host", raw): self.XML_NDO = "xml/broker" else: self.XML_NDO = "xml/broker" del raw else: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug = "Could not detect host/service status version. Using Centreon_Broker") # some cleanup del result, error def _get_host_id(self, host): """ get host_id via parsing raw html """ cgi_data = urllib.urlencode({"p":201,\ "o":"hd", "host_name":host}) result = self.FetchURL(self.monitor_cgi_url + "/main.php?" + cgi_data, cgi_data=urllib.urlencode({"sid":self.SID}), giveback="raw") raw, error = result.result, result.error if error == "": host_id = raw.partition("var host_id = '")[2].partition("'")[0] del raw else: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug = "Host ID could not be retrieved.") # some cleanup del result, error # only if host_id is an usable integer return it try: if int(host_id): if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug = "Host ID is " + host_id) return host_id else: return "" except: return "" def _get_host_and_service_id(self, host, service): """ parse a ton of html to get a host and a service id... """ cgi_data = urllib.urlencode({"p":"20218",\ "host_name":host,\ "service_description":service,\ "o":"as"}) # might look strange to have cgi_data 2 times, the first it is the "real" in URL and the second is the cgi_data parameter # from urllib to get the session id POSTed result = self.FetchURL(self.monitor_cgi_url + "/main.php?"+ cgi_data, cgi_data=urllib.urlencode({"sid":self.SID}), giveback="raw") raw, error = result.result, result.error # ids to give back, should contain two items, a host and a service id ids = [] if error == "": # search ids for l in raw.splitlines(): if l.find('selected="selected"') <> -1: ids.append(l.split('value="')[1].split('"')[0]) else: return ids else: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, service=service, debug = "IDs could not be retrieved.") return "", "" def _get_status(self): """ Get status from Centreon Server """ # get sid in case this has not yet been done if self.SID == None or self.SID == "": self.SID = self._get_sid().result # those ndo urls would not be changing too often so this check migth be done here self._get_ndo_url() # services (unknown, warning or critical?) nagcgiurl_services = self.monitor_cgi_url + "/include/monitoring/status/Services/" + self.XML_NDO + "/serviceXML.php?" + urllib.urlencode({"num":0, "limit":999, "o":"svcpb", "sort_type":"status", "sid":self.SID}) # hosts (up or down or unreachable) nagcgiurl_hosts = self.monitor_cgi_url + "/include/monitoring/status/Hosts/" + self.XML_NDO + "/hostXML.php?" + urllib.urlencode({"num":0, "limit":999, "o":"hpb", "sort_type":"status", "sid":self.SID}) # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: result = self.FetchURL(nagcgiurl_hosts, giveback="xml") xmlobj, error = result.result, result.error if error != "": return Result(result=copy.deepcopy(xmlobj), error=copy.deepcopy(error)) # in case there are no children session id is invalid if xmlobj == "bad session id" or str(xmlobj) == "Bad Session ID": del xmlobj if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Bad session ID, retrieving new one...") # try again... self.SID = self._get_sid().result result = self.FetchURL(nagcgiurl_hosts, giveback="xml") xmlobj, error = result.result, result.error if error != "": return Result(result=copy.deepcopy(xmlobj), error=copy.deepcopy(error)) # a second time a bad session id should raise an error if xmlobj == "bad session id" or str(xmlobj) == "Bad Session ID": return Result(result="ERROR", error=str(xmlobj)) for l in xmlobj.findAll("l"): try: # host objects contain service objects if not self.new_hosts.has_key(str(l.hn.text)): self.new_hosts[str(l.hn.text)] = GenericHost() self.new_hosts[str(l.hn.text)].name = str(l.hn.text) self.new_hosts[str(l.hn.text)].server = self.name self.new_hosts[str(l.hn.text)].status = str(l.cs.text) # disgusting workaround for https://github.com/HenriWahl/Nagstamon/issues/91 if self.new_hosts[str(l.hn.text)].status in self.TRANSLATIONS: self.new_hosts[str(l.hn.text)].status = self.TRANSLATIONS[self.new_hosts[str(l.hn.text)].status] self.new_hosts[str(l.hn.text)].attempt, self.new_hosts[str(l.hn.text)].status_type = str(l.tr.text).split(" ") self.new_hosts[str(l.hn.text)].status_type = self.HARD_SOFT[self.new_hosts[str(l.hn.text)].status_type] self.new_hosts[str(l.hn.text)].last_check = str(l.lc.text) self.new_hosts[str(l.hn.text)].duration = str(l.lsc.text) self.new_hosts[str(l.hn.text)].status_information= str(l.ou.text) if l.find("cih") != None: self.new_hosts[str(l.hn.text)].criticality = str(l.cih.text) else: self.new_hosts[str(l.hn.text)].criticality = "" self.new_hosts[str(l.hn.text)].acknowledged = bool(int(str(l.ha.text))) self.new_hosts[str(l.hn.text)].scheduled_downtime = bool(int(str(l.hdtm.text))) if l.find("is") != None: self.new_hosts[str(l.hn.text)].flapping = bool(int(str(l.find("is").text))) else: self.new_hosts[str(l.hn.text)].flapping = False self.new_hosts[str(l.hn.text)].notifications_disabled = not bool(int(str(l.ne.text))) self.new_hosts[str(l.hn.text)].passiveonly = not bool(int(str(l.ace.text))) except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) del xmlobj except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # services try: result = self.FetchURL(nagcgiurl_services, giveback="xml") xmlobj, error = result.result, result.error if error != "": return Result(result=xmlobj, error=copy.deepcopy(error)) # in case there are no children session id is invalid if xmlobj == "bad session id" or xmlobj == "Bad Session ID": # debug if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Bad session ID, retrieving new one...") # try again... self.SID = self._get_sid().result result = self.FetchURL(nagcgiurl_services, giveback="xml") xmlobj, error = result.result, result.error if error != "": return Result(result="ERROR", error=copy.deepcopy(error)) for l in xmlobj.findAll("l"): try: # host objects contain service objects if not self.new_hosts.has_key(str(l.hn.text)): self.new_hosts[str(l.hn.text)] = GenericHost() self.new_hosts[str(l.hn.text)].name = str(l.hn.text) self.new_hosts[str(l.hn.text)].status = "UP" # if a service does not exist create its object if not self.new_hosts[str(l.hn.text)].services.has_key(str(l.sd.text)): self.new_hosts[str(l.hn.text)].services[str(l.sd.text)] = GenericService() self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].host = str(l.hn.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].name = str(l.sd.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].server = self.name self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status = str(l.cs.text) # disgusting workaround for https://github.com/HenriWahl/Nagstamon/issues/91 if self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status in self.TRANSLATIONS: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status = self.TRANSLATIONS[\ self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status] self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].attempt, \ self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type = str(l.ca.text).split(" ") self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type =\ self.HARD_SOFT[self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_type] self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].last_check = str(l.lc.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].duration = str(l.d.text) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].status_information = str(l.po.text).replace("\n", " ").strip() if l.find("cih") != None: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].criticality = str(l.cih.text) else: self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].criticality = "" self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].acknowledged = bool(int(str(l.pa.text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].scheduled_downtime = bool(int(str(l.dtm.text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].flapping = bool(int(str(l.find("is").text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].notifications_disabled = not bool(int(str(l.ne.text))) self.new_hosts[str(l.hn.text)].services[str(l.sd.text)].passiveonly = not bool(int(str(l.ac.text))) except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del xmlobj except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # return True if all worked well return Result() def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=[]): # decision about host or service - they have different URLs try: if service == "": # host cgi_data = urllib.urlencode({"p":"20105", "cmd":"14", "host_name":host, \ "author":author, "comment":comment, "submit":"Add", "notify":int(notify),\ "persistent":int(persistent), "sticky":int(sticky), "ackhostservice":"0", "o":"hd", "en":"1"}) # debug if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug=self.monitor_cgi_url + "/main.php?"+ cgi_data) # running remote cgi command, also possible with GET method raw = self.FetchURL(self.monitor_cgi_url + "/main.php?" + cgi_data, giveback="raw") del raw # if host is acknowledged and all services should be to or if a service is acknowledged # (and all other on this host too) if service != "" or len(all_services) > 0: # service(s) @ host # if all_services is empty only one service has to be checked - the one clicked # otherwise if there all services should be acknowledged if len(all_services) == 0: all_services = [service] # acknowledge all services on a host for s in all_services: # service @ host # in case the Centreon guys one day fix their typos "persistent" and # "persistent" will both be given (it is "persistant" in scheduling for downtime) cgi_data = urllib.urlencode({"p":"20215", "cmd":"15", "host_name":host, \ "author":author, "comment":comment, "submit":"Add", "notify":int(notify),\ "service_description":s, "force_check":"1", \ "persistent":int(persistent), "persistant":int(persistent),\ "sticky":int(sticky), "o":"svcd", "en":"1"}) # debug if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, service=s, debug=self.monitor_cgi_url + "/main.php?" + cgi_data) # running remote cgi command with GET method, for some strange reason only working if # giveback is "raw" raw = self.FetchURL(self.monitor_cgi_url + "/main.php?" + cgi_data, giveback="raw") del raw except: self.Error(sys.exc_info()) def _set_recheck(self, host, service): """ host and service ids are needed to tell Centreon what whe want """ # yes this procedure IS resource waste... suggestions welcome! try: # decision about host or service - they have different URLs if service == "": # ... it can only be a host, get its id host_id = self._get_host_id(host) # fill and encode CGI data cgi_data = urllib.urlencode({"cmd":"host_schedule_check", "actiontype":1,\ "host_id":host_id, "sid":self.SID}) url = self.monitor_cgi_url + "/include/monitoring/objectDetails/xml/hostSendCommand.php?" + cgi_data del host_id else: # service @ host host_id, service_id = self._get_host_and_service_id(host, service) # fill and encode CGI data cgi_data = urllib.urlencode({"cmd":"service_schedule_check", "actiontype":1,\ "host_id":host_id, "service_id":service_id, "sid":self.SID}) url = self.monitor_cgi_url + "/include/monitoring/objectDetails/xml/serviceSendCommand.php?" + cgi_data del host_id, service_id # execute POST request raw = self.FetchURL(url, giveback="raw") del raw except: self.Error(sys.exc_info()) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): """ gets actual host and service ids and apply them to downtime cgi """ try: if service == "": # host host_id = self._get_host_id(host) cgi_data = urllib.urlencode({"p":"20106",\ "host_id":host_id,\ "host_or_hg[host_or_hg]":1,\ "submitA":"Save",\ "persistent":int(fixed),\ "persistant":int(fixed),\ "start":start_time,\ "end":end_time,\ "comment":comment,\ "o":"ah"}) # debug if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug=self.monitor_cgi_url + "/main.php?" + cgi_data) else: # service host_id, service_id = self._get_host_and_service_id(host, service) cgi_data = urllib.urlencode({"p":"20218",\ "host_id":host_id,\ "service_id":service_id,\ "submitA":"Save",\ "persistant":int(fixed),\ "start":start_time,\ "end":end_time,\ "comment":comment,\ "o":"as"}) # debug if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, service=service, debug=self.monitor_cgi_url + "/main.php?" + cgi_data) # running remote cgi command raw = self.FetchURL(self.monitor_cgi_url + "/main.php", giveback="raw", cgi_data=cgi_data) del raw except: self.Error(sys.exc_info()) def Hook(self): """ in case count is down get a new SID, just in case was kicked out but as to be seen in https://sourceforge.net/p/nagstamon/bugs/86/ there are problems with older Centreon installations so this should come back """ # renewing the SID once an hour might be enough # maybe this is unnecessary now that we authenticate via login/password, no md5 if self.SIDcount >= 3600: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Old SID: " + self.SID + " " + str(self.Cookie)) self.SID = self._get_sid().result if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="New SID: " + self.SID + " " + str(self.Cookie)) self.SIDcount = 0 else: self.SIDcount += 1 Nagstamon/Nagstamon/Server/Generic.py000066400000000000000000001616111240775040100201360ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import urllib import urllib2 import cookielib import sys import socket import copy import webbrowser import datetime import time import traceback import base64 import re import gobject # to let Linux distributions use their own BeautifulSoup if existent try importing local BeautifulSoup first # see https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3302612&group_id=236865 try: from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup except: from Nagstamon.thirdparty.BeautifulSoup import BeautifulSoup,\ BeautifulStoneSoup from Nagstamon.Actions import HostIsFilteredOutByRE,\ ServiceIsFilteredOutByRE,\ StatusInformationIsFilteredOutByRE,\ CriticalityIsFilteredOutByRE,\ not_empty from Nagstamon.Objects import * class GenericServer(object): """ Abstract server which serves as template for all other types Default values are for Nagios servers """ TYPE = 'Generic' # GUI sortable columns stuff HOST_COLUMN_ID = 0 SERVICE_COLUMN_ID = 1 STATUS_COLUMN_ID = 2 LAST_CHECK_COLUMN_ID = 3 DURATION_COLUMN_ID = 4 ATTEMPT_COLUMN_ID = 5 # used for $STATUS$ variable for custom actions STATUS_INFO_COLUMN_ID = 6 COLUMNS = [ HostColumn, ServiceColumn, StatusColumn, LastCheckColumn, DurationColumn, AttemptColumn, StatusInformationColumn ] DISABLED_CONTROLS = [] # dictionary to translate status bitmaps on webinterface into status flags # this are defaults from Nagios # "disabled.gif" is in Nagios for hosts the same as "passiveonly.gif" for services STATUS_MAPPING = { "ack.gif" : "acknowledged",\ "passiveonly.gif" : "passiveonly",\ "disabled.gif" : "passiveonly",\ "ndisabled.gif" : "notifications_disabled",\ "downtime.gif" : "scheduled_downtime",\ "flapping.gif" : "flapping"} # Entries for monitor default actions in context menu MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Submit check result", "Downtime"] # Arguments available for submitting check results SUBMIT_CHECK_RESULT_ARGS = ["check_output", "performance_data"] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS = { "monitor": "$MONITOR$",\ "hosts": "$MONITOR-CGI$/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12",\ "services": "$MONITOR-CGI$/status.cgi?host=all&servicestatustypes=253",\ "history": "$MONITOR-CGI$/history.cgi?host=all"} def __init__(self, **kwds): # add all keywords to object, every mode searchs inside for its favorite arguments/keywords for k in kwds: self.__dict__[k] = kwds[k] self.type = "" self.monitor_url = "" self.monitor_cgi_url = "" self.username = "" self.password = "" self.use_proxy = False self.use_proxy_from_os = False self.proxy_address = "" self.proxy_username = "" self.proxy_password = "" self.hosts = dict() self.new_hosts = dict() self.thread = None self.isChecking = False self.CheckingForNewVersion = False self.WorstStatus = "UP" self.States = ["UP", "UNKNOWN", "WARNING", "CRITICAL", "UNREACHABLE", "DOWN"] self.nagitems_filtered_list = list() self.nagitems_filtered = {"services":{"CRITICAL":[], "WARNING":[], "UNKNOWN":[]}, "hosts":{"DOWN":[], "UNREACHABLE":[]}} self.downs = 0 self.unreachables = 0 self.unknowns = 0 self.criticals = 0 self.warnings = 0 self.status = "" self.status_description = "" # needed for looping server thread self.count = 0 # needed for RecheckAll - save start_time once for not having to get it for every recheck self.start_time = None self.Cookie = cookielib.CookieJar() # use server-owned attributes instead of redefining them with every request self.passman = None self.basic_handler = None self.digest_handler = None self.proxy_handler = None self.proxy_auth_handler = None self.urlopener = None # headers for HTTP requests, might be needed for authorization on Nagios/Icinga Hosts self.HTTPheaders = dict() # attempt to use only one bound list of TreeViewColumns instead of ever increasing one self.TreeView = None self.TreeViewColumns = list() self.ListStore = None self.ListStoreColumns = list() # flag which decides if authentication has to be renewed self.refresh_authentication = False # to handle Icinga versions this information is necessary, might be of future use for others too self.version = "" # Special FX # Centreon self.use_autologin = False self.autologin_key = "" # Icinga self.use_display_name_host = False self.use_display_name_service = False def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # create filters like described in # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes # # the following variables are not necessary anymore as with "new" filtering # # hoststatus #hoststatustypes = 12 # servicestatus #servicestatustypes = 253 # serviceprops & hostprops both have the same values for the same states so I # group them together #hostserviceprops = 0 # services (unknown, warning or critical?) as dictionary, sorted by hard and soft state type self.cgiurl_services = {"hard": self.monitor_cgi_url + "/status.cgi?host=all&servicestatustypes=253&serviceprops=262144&limit=0",\ "soft": self.monitor_cgi_url + "/status.cgi?host=all&servicestatustypes=253&serviceprops=524288&limit=0"} # hosts (up or down or unreachable) self.cgiurl_hosts = { "hard": self.monitor_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=262144&limit=0",\ "soft": self.monitor_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=524288&limit=0"} def init_HTTP(self): """ partly not constantly working Basic Authorization requires extra Authorization headers, different between various server types """ if self.HTTPheaders == {}: for giveback in ["raw", "obj"]: self.HTTPheaders[giveback] = {"Authorization": "Basic " + base64.b64encode(self.username + ":" + self.password)} def reset_HTTP(self): """ if authentication fails try to reset any HTTP session stuff - might be different for different monitors """ self.HTTPheaders = dict() def get_name(self): """ return stringified name """ return str(self.name) def get_username(self): """ return stringified username """ return str(self.username) def get_password(self): """ return stringified password """ return str(self.password) @classmethod def get_columns(cls, row): """ Gets columns filled with row data """ for column_class in cls.COLUMNS: # str() necessary because MacOSX Python cries otherwise yield str(column_class(row)) def get_server_version(self): """ dummy function, at the moment only used by Icinga """ pass def set_recheck(self, thread_obj): self._set_recheck(thread_obj.host, thread_obj.service) def _set_recheck(self, host, service): if service != "": if self.hosts[host].services[service].is_passive_only(): # Do not check passive only checks return # get start time from Nagios as HTML to use same timezone setting like the locally installed Nagios result = self.FetchURL(self.monitor_cgi_url + "/cmd.cgi?" + urllib.urlencode({"cmd_typ":"96", "host":host})) self.start_time = dict(result.result.find(attrs={"name":"start_time"}).attrs)["value"] # decision about host or service - they have different URLs if service == "": # host cmd_typ = "96" else: # service @ host cmd_typ = "7" # ignore empty service in case of rechecking a host cgi_data = urllib.urlencode([("cmd_typ", cmd_typ),\ ("cmd_mod", "2"),\ ("host", host),\ ("service", service),\ ("start_time", self.start_time),\ ("force_check", "on"),\ ("btnSubmit", "Commit")]) # execute POST request self.FetchURL(self.monitor_cgi_url + "/cmd.cgi", giveback="raw", cgi_data=cgi_data) def set_acknowledge(self, thread_obj): if thread_obj.acknowledge_all_services == True: all_services = thread_obj.all_services else: all_services = [] self._set_acknowledge(thread_obj.host, thread_obj.service, thread_obj.author, thread_obj.comment,\ thread_obj.sticky, thread_obj.notify, thread_obj.persistent, all_services) # resfresh immediately according to https://github.com/HenriWahl/Nagstamon/issues/86 self.thread.doRefresh = True def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=[]): url = self.monitor_cgi_url + "/cmd.cgi" # decision about host or service - they have different URLs # do not care about the doube %s (%s%s) - its ok, "flags" cares about the necessary "&" if service == "": # host # according to sf.net bug #3304098 (https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3304098&group_id=236865) # the send_notification-flag must not exist if it is set to "off", otherwise # the Nagios core interpretes it as set, regardless its real value if notify == True: send_notification = "&send_notification=on" else: send_notification = "" # dito for persistence... if persistent == True: persistent_comment = "&persistent=on" else: persistent_comment = "" # ...and sticky acks too? if sticky == True: sticky_ack = "&sticky_ack=on" else: sticky_ack = "" cgi_data = urllib.urlencode([("cmd_typ","33"), ("cmd_mod","2"), ("host",host), ("com_author",author),\ ("com_data",comment), ("btnSubmit","Commit")])\ + send_notification + persistent_comment + sticky_ack self.FetchURL(url, giveback="raw", cgi_data=cgi_data) # if host is acknowledged and all services should be to or if a service is acknowledged # (and all other on this host too) if service != "": # service @ host # the same applies here as with the host and send_notification if notify == True: send_notification = "&send_notification=on" else: send_notification = "" # dito for persistence... if persistent == True: persistent_comment = "&persistent=on" else: persistent_comment = "" # ...and sticky acks too? if sticky == True: sticky_ack = "&sticky_ack=on" else: sticky_ack = "" # for whatever silly reason Icinga depends on the correct order of submitted form items... # see sf.net bug 3428844 # so whe cannot use a dictionary with urllib but a tuple full of tuples cgi_data = urllib.urlencode([("cmd_typ","34"), ("cmd_mod","2"), ("host",host), ("service",service),\ ("com_author",author), ("com_data",comment), ("btnSubmit","Commit")])\ + send_notification + persistent_comment + sticky_ack # running remote cgi command self.FetchURL(url, giveback="raw", cgi_data=cgi_data) # acknowledge all services on a host if len(all_services) > 0: for s in all_services: # services @ host # the same applies here as with the host and send_notification if notify == True: send_notification = "&send_notification=on" else: send_notification = "" # dito for persistence... if persistent == True: persistent_comment = "&persistent=on" else: persistent_comment = "" # ...and sticky acks too? if sticky == True: sticky_ack = "&sticky_ack=on" else: sticky_ack = "" cgi_data = urllib.urlencode([("cmd_typ","34"), ("cmd_mod","2"), ("host",host), ("service",s),\ ("com_author",author), ("com_data",comment), ("btnSubmit","Commit")])\ + send_notification + persistent_comment + sticky_ack #running remote cgi command self.FetchURL(url, giveback="raw", cgi_data=cgi_data) def set_downtime(self, thread_obj): self._set_downtime(thread_obj.host, thread_obj.service, thread_obj.author, thread_obj.comment, thread_obj.fixed, thread_obj.start_time, thread_obj.end_time, thread_obj.hours, thread_obj.minutes) # resfresh immediately according to https://github.com/HenriWahl/Nagstamon/issues/86 self.thread.doRefresh = True def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): # decision about host or service - they have different URLs if service == "": # host cmd_typ = "55" else: # service @ host cmd_typ = "56" # for some reason Icinga is very fastidiuos about the order of CGI arguments, so please # here we go... it took DAYS :-( cgi_data = urllib.urlencode([("cmd_typ", cmd_typ),\ ("cmd_mod", "2"),\ ("trigger", "0"),\ ("childoptions", "0"),\ ("host", host),\ ("service", service),\ ("com_author", author),\ ("com_data", comment),\ ("fixed", fixed),\ ("start_time", start_time),\ ("end_time", end_time),\ ("hours", hours),\ ("minutes", minutes),\ ("btnSubmit","Commit")]) # running remote cgi command self.FetchURL(self.monitor_cgi_url + "/cmd.cgi", giveback="raw", cgi_data=cgi_data) def set_submit_check_result(self, thread_obj): self._set_submit_check_result(thread_obj.host, thread_obj.service, thread_obj.state, thread_obj.comment,\ thread_obj.check_output, thread_obj.performance_data) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): """ worker for submitting check result """ url = self.monitor_cgi_url + "/cmd.cgi" # decision about host or service - they have different URLs if service == "": # host cgi_data = urllib.urlencode([("cmd_typ","87"), ("cmd_mod","2"), ("host",host),\ ("plugin_state",{"up":"0", "down":"1", "unreachable":"2"}[state]),\ ("plugin_output",check_output),\ ("performance_data",performance_data), ("btnSubmit","Commit")]) self.FetchURL(url, giveback="raw", cgi_data=cgi_data) if service != "": # service @ host cgi_data = urllib.urlencode([("cmd_typ","30"), ("cmd_mod","2"), ("host",host), ("service",service),\ ("plugin_state",{"ok":"0", "warning":"1", "critical":"2", "unknown":"3"}[state]), ("plugin_output",check_output),\ ("performance_data",performance_data), ("btnSubmit","Commit")]) # running remote cgi command self.FetchURL(url, giveback="raw", cgi_data=cgi_data) def get_start_end(self, host): """ for GUI to get actual downtime start and end from server - they may vary so it's better to get directly from web interface """ try: result = self.FetchURL(self.monitor_cgi_url + "/cmd.cgi?" + urllib.urlencode({"cmd_typ":"55", "host":host})) start_time = dict(result.result.find(attrs={"name":"start_time"}).attrs)["value"] end_time = dict(result.result.find(attrs={"name":"end_time"}).attrs)["value"] # give values back as tuple return start_time, end_time except: self.Error(sys.exc_info()) return "n/a", "n/a" def open_tree_view(self, host, service=""): """ open monitor from treeview context menu """ # only type is important so do not care of service "" in case of host monitor if service == "": typ = 1 else: typ = 2 if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, service=service, debug="Open host/service monitor web page " + self.monitor_cgi_url + '/extinfo.cgi?' + urllib.urlencode({"type":typ, "host":host, "service":service})) webbrowser.open(self.monitor_cgi_url + '/extinfo.cgi?' + urllib.urlencode({"type":typ, "host":host, "service":service})) def OpenBrowser(self, widget=None, url_type="", output=None): """ multiple purpose open browser method for all open-a-browser-needs """ # first close popwin if output <> None: output.popwin.Close() # run thread with action action = Actions.Action(string=self.BROWSER_URLS[url_type],\ type="browser",\ conf=self.conf,\ server=self) action.run() def _get_status(self): """ Get status from Nagios Server """ # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily nagitems = {"services":[], "hosts":[]} # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: for status_type in "hard", "soft": result = self.FetchURL(self.cgiurl_hosts[status_type]) htobj, error = result.result, result.error if error != "": return Result(result=copy.deepcopy(htobj), error=copy.deepcopy(error)) # put a copy of a part of htobj into table to be able to delete htobj # too mnuch copy.deepcopy()s here give recursion crashs table = htobj('table', {'class': 'status'})[0] # access table rows # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) # do some cleanup del result, error # kick out table heads trs.pop(0) # dummy tds to be deleteable tds = [] for tr in trs: try: # ignore empty rows if len(tr('td', recursive=False)) > 1: n = dict() # get tds in one tr tds = tr('td', recursive=False) # host try: n["host"] = str(tds[0].table.tr.td.table.tr.td.a.string) except: n["host"] = str(nagitems[len(nagitems)-1]["host"]) # status n["status"] = str(tds[1].string) # last_check n["last_check"] = str(tds[2].string) # duration n["duration"] = str(tds[3].string) # division between Nagios and Icinga in real life... where # Nagios has only 5 columns there are 7 in Icinga 1.3... # ... and 6 in Icinga 1.2 :-) if len(tds) < 7: # the old Nagios table # status_information if len(tds[4](text=not_empty)) == 0: n["status_information"] = "" else: n["status_information"] = str(tds[4].string).encode("utf-8").replace("\n", " ").strip() # attempts are not shown in case of hosts so it defaults to "N/A" n["attempt"] = "N/A" else: # attempts are shown for hosts # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n["attempt"] = str(tds[4].string).strip() # status_information if len(tds[5](text=not_empty)) == 0: n["status_information"] = "" else: n["status_information"] = str(tds[5].string).encode("utf-8").replace("\n", " ").strip() # status flags n["passiveonly"] = False n["notifications_disabled"] = False n["flapping"] = False n["acknowledged"] = False n["scheduled_downtime"] = False # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i["src"].split("/")[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this host item to nagitems nagitems["hosts"].append(n) # after collection data in nagitems create objects from its informations # host objects contain service objects if not self.new_hosts.has_key(n["host"]): new_host = n["host"] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n["host"] self.new_hosts[new_host].server = self.name self.new_hosts[new_host].status = n["status"] self.new_hosts[new_host].last_check = n["last_check"] self.new_hosts[new_host].duration = n["duration"] self.new_hosts[new_host].attempt = n["attempt"] self.new_hosts[new_host].status_information= n["status_information"].encode("utf-8") self.new_hosts[new_host].passiveonly = n["passiveonly"] self.new_hosts[new_host].notifications_disabled = n["notifications_disabled"] self.new_hosts[new_host].flapping = n["flapping"] self.new_hosts[new_host].acknowledged = n["acknowledged"] self.new_hosts[new_host].scheduled_downtime = n["scheduled_downtime"] self.new_hosts[new_host].status_type = status_type del tds, n except: self.Error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in "hard", "soft": result = self.FetchURL(self.cgiurl_services[status_type]) htobj, error = result.result, result.error if error != "": return Result(result=copy.deepcopy(htobj), error=copy.deepcopy(error)) # too much copy.deepcopy()s here give recursion crashs table = htobj('table', {'class': 'status'})[0] # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) del result, error # kick out table heads trs.pop(0) # dummy tds to be deleteable tds = [] for tr in trs: try: # ignore empty rows - there are a lot of them - a Nagios bug? tds = tr('td', recursive=False) if len(tds) > 1: n = dict() # host # the resulting table of Nagios status.cgi table omits the # hostname of a failing service if there are more than one # so if the hostname is empty the nagios status item should get # its hostname from the previuos item - one reason to keep "nagitems" try: n["host"] = str(tds[0](text=not_empty)[0]) except: n["host"] = str(nagitems["services"][len(nagitems["services"])-1]["host"]) # service n["service"] = str(tds[1](text=not_empty)[0]) # status n["status"] = str(tds[2](text=not_empty)[0]) # last_check n["last_check"] = str(tds[3](text=not_empty)[0]) # duration n["duration"] = str(tds[4](text=not_empty)[0]) # attempt # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n["attempt"] = str(tds[5](text=not_empty)[0]).strip() # status_information if len(tds[6](text=not_empty)) == 0: n["status_information"] = "" else: n["status_information"] = str(tds[6](text=not_empty)[0]).encode("utf-8") # status flags n["passiveonly"] = False n["notifications_disabled"] = False n["flapping"] = False n["acknowledged"] = False n["scheduled_downtime"] = False # map status icons to status flags icons = tds[1].findAll('img') for i in icons: icon = i["src"].split("/")[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this service item to nagitems - only if service nagitems["services"].append(n) # after collection data in nagitems create objects of its informations # host objects contain service objects if not self.new_hosts.has_key(n["host"]): self.new_hosts[n["host"]] = GenericHost() self.new_hosts[n["host"]].name = n["host"] self.new_hosts[n["host"]].status = "UP" # trying to fix https://sourceforge.net/tracker/index.php?func=detail&aid=3299790&group_id=236865&atid=1101370 # if host is not down but in downtime or any other flag this should be evaluated too # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i["src"].split("/")[-1] if icon in self.STATUS_MAPPING: self.new_hosts[n["host"]].__dict__[self.STATUS_MAPPING[icon]] = True # if a service does not exist create its object if not self.new_hosts[n["host"]].services.has_key(n["service"]): new_service = n["service"] self.new_hosts[n["host"]].services[new_service] = GenericService() self.new_hosts[n["host"]].services[new_service].host = n["host"] self.new_hosts[n["host"]].services[new_service].name = n["service"] self.new_hosts[n["host"]].services[new_service].server = self.name self.new_hosts[n["host"]].services[new_service].status = n["status"] self.new_hosts[n["host"]].services[new_service].last_check = n["last_check"] self.new_hosts[n["host"]].services[new_service].duration = n["duration"] self.new_hosts[n["host"]].services[new_service].attempt = n["attempt"] self.new_hosts[n["host"]].services[new_service].status_information = n["status_information"].encode("utf-8") self.new_hosts[n["host"]].services[new_service].passiveonly = n["passiveonly"] self.new_hosts[n["host"]].services[new_service].notifications_disabled = n["notifications_disabled"] self.new_hosts[n["host"]].services[new_service].flapping = n["flapping"] self.new_hosts[n["host"]].services[new_service].acknowledged = n["acknowledged"] self.new_hosts[n["host"]].services[new_service].scheduled_downtime = n["scheduled_downtime"] self.new_hosts[n["host"]].services[new_service].status_type = status_type del tds, n except: self.Error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del nagitems #dummy return in case all is OK return Result() def GetStatus(self, output=None): """ get nagios status information from cgiurl and give it back as dictionary output parameter is needed in case authentication failed so that popwin might ask for credentials """ # set checking flag to be sure only one thread cares about this server self.isChecking = True # check if server is enabled, if not, do not get any status if str(self.conf.servers[self.get_name()].enabled) == "False": self.WorstStatus = "UP" self.isChecking = False return Result() # get all trouble hosts/services from server specific _get_status() status = self._get_status() self.status, self.status_description = status.result, status.error if status.error != "": # ask for password if authorization failed if "HTTP Error 401" in status.error or \ "HTTP Error 403" in status.error or \ "HTTP Error 500" in status.error or \ "bad session id" in status.error.lower() or \ "login failed" in status.error.lower(): if str(self.conf.servers[self.name].enabled) == "True": # needed to get valid credentials self.refresh_authentication = True while status.error != "": gobject.idle_add(output.RefreshDisplayStatus) # clean existent authentication self.reset_HTTP() self.init_HTTP() status = self._get_status() self.status, self.status_description = status.result, status.error # take a break not to DOS the monitor... time.sleep(10) # if monitor has been disabled do not try to connect to it if str(self.conf.servers[self.name].enabled) == "False": break # if reauthentication did not work already try again to get correct credentials self.refresh_authentication = True else: self.isChecking = False return Result(result=self.status, error=self.status_description) # no rew authentication needed self.refresh_authentication = False # this part has been before in GUI.RefreshDisplay() - wrong place, here it needs to be reset self.nagitems_filtered = {"services":{"CRITICAL":[], "WARNING":[], "UNKNOWN":[]}, "hosts":{"DOWN":[], "UNREACHABLE":[]}} # initialize counts for various service/hosts states # count them with every miserable host/service respective to their meaning self.downs = 0 self.unreachables = 0 self.unknowns = 0 self.criticals = 0 self.warnings = 0 for host in self.new_hosts.values(): # Don't enter the loop if we don't have a problem. Jump down to your problem services if not host.status == "UP": # Some generic filters if host.acknowledged == True and str(self.conf.filter_acknowledged_hosts_services) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: ACKNOWLEDGED " + str(host.name)) host.visible = False if host.notifications_disabled == True and str(self.conf.filter_hosts_services_disabled_notifications) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: NOTIFICATIONS " + str(host.name)) host.visible = False if host.passiveonly == True and str(self.conf.filter_hosts_services_disabled_checks) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: PASSIVEONLY " + str(host.name)) host.visible = False if host.scheduled_downtime == True and str(self.conf.filter_hosts_services_maintenance) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: DOWNTIME " + str(host.name)) host.visible = False if host.flapping == True and str(self.conf.filter_all_flapping_hosts) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: FLAPPING HOST " + str(host.name)) host.visible = False # Check_MK and OP5 do not show the status_type so their host.status_type will be empty if host.status_type != "": if str(self.conf.filter_hosts_in_soft_state) == "True" and host.status_type == "soft": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: SOFT STATE " + str(host.name)) host.visible = False if HostIsFilteredOutByRE(host.name, self.conf) == True: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP " + str(host.name)) host.visible = False if StatusInformationIsFilteredOutByRE(host.status_information, self.conf) == True: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP " + str(host.name)) host.visible = False #The Criticality filter can be used only with centreon objects. Other objects don't have the criticality attribute. if (str(self.type) == "Centreon") and (CriticalityIsFilteredOutByRE(host.criticality, self.conf) == True): if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP Criticality " + str(host.name)) host.visible = False # Finegrain for the specific state if host.status == "DOWN": if str(self.conf.filter_all_down_hosts) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: DOWN " + str(host.name)) host.visible = False if host.visible: self.nagitems_filtered["hosts"]["DOWN"].append(host) self.downs += 1 if host.status == "UNREACHABLE": if str(self.conf.filter_all_unreachable_hosts) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: UNREACHABLE " + str(host.name)) host.visible = False if host.visible: self.nagitems_filtered["hosts"]["UNREACHABLE"].append(host) self.unreachables += 1 for service in host.services.values(): # Some generic filtering if service.acknowledged == True and str(self.conf.filter_acknowledged_hosts_services) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: ACKNOWLEDGED " + str(host.name) + ";" + str(service.name)) service.visible = False if service.notifications_disabled == True and str(self.conf.filter_hosts_services_disabled_notifications) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: NOTIFICATIONS " + str(host.name) + ";" + str(service.name)) service.visible = False if service.passiveonly == True and str(self.conf.filter_hosts_services_disabled_checks) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: PASSIVEONLY " + str(host.name) + ";" + str(service.name)) service.visible = False if service.scheduled_downtime == True and str(self.conf.filter_hosts_services_maintenance) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: DOWNTIME " + str(host.name) + ";" + str(service.name)) service.visible = False if service.flapping == True and str(self.conf.filter_all_flapping_services) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: FLAPPING SERVICE " + str(host.name) + ";" + str(service.name)) service.visible = False if host.scheduled_downtime == True and str(self.conf.filter_services_on_hosts_in_maintenance) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: Service on host in DOWNTIME " + str(host.name) + ";" + str(service.name)) service.visible = False if host.acknowledged == True and str(self.conf.filter_services_on_acknowledged_hosts) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: Service on acknowledged host" + str(host.name) + ";" + str(service.name)) service.visible = False if host.status == "DOWN" and str(self.conf.filter_services_on_down_hosts) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: Service on host in DOWN " + str(host.name) + ";" + str(service.name)) service.visible = False if host.status == "UNREACHABLE" and str(self.conf.filter_services_on_unreachable_hosts) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: Service on host in UNREACHABLE " + str(host.name) + ";" + str(service.name)) service.visible = False # Check_MK and OP5 do not show the status_type so their host.status_type will be empty if service.status_type != "": if str(self.conf.filter_services_in_soft_state) == "True" and service.status_type == "soft": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: SOFT STATE " + str(host.name) + ";" + str(service.name)) service.visible = False else: # the old, actually wrong, behaviour real_attempt, max_attempt = service.attempt.split("/") if real_attempt <> max_attempt and str(self.conf.filter_services_in_soft_state) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: SOFT STATE " + str(host.name) + ";" + str(service.name)) service.visible = False if HostIsFilteredOutByRE(host.name, self.conf) == True: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP " + str(host.name) + ";" + str(service.name)) service.visible = False if ServiceIsFilteredOutByRE(service.get_name(), self.conf) == True: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP " + str(host.name) + ";" + str(service.name)) service.visible = False if StatusInformationIsFilteredOutByRE(service.status_information, self.conf) == True: if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP " + str(host.name) + ";" + str(service.name)) service.visible = False #The Criticality filter can be used only with centreon objects. Other objects don't have the criticality attribute. if (str(self.type) == "Centreon") and (CriticalityIsFilteredOutByRE(service.criticality, self.conf) == True): if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: REGEXP Criticality %s;%s %s" % ((str(host.name), str(service.name), str(service.criticality)))) service.visible = False # Finegrain for the specific state if service.visible: if service.status == "CRITICAL": if str(self.conf.filter_all_critical_services) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: CRITICAL " + str(host.name) + ";" + str(service.name)) service.visible = False else: self.nagitems_filtered["services"]["CRITICAL"].append(service) self.criticals += 1 if service.status == "WARNING": if str(self.conf.filter_all_warning_services) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: WARNING " + str(host.name) + ";" + str(service.name)) service.visible = False else: self.nagitems_filtered["services"]["WARNING"].append(service) self.warnings += 1 if service.status == "UNKNOWN": if str(self.conf.filter_all_unknown_services) == "True": if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Filter: UNKNOWN " + str(host.name) + ";" + str(service.name)) service.visible = False else: self.nagitems_filtered["services"]["UNKNOWN"].append(service) self.unknowns += 1 # find out if there has been some status change to notify user # compare sorted lists of filtered nagios items new_nagitems_filtered_list = [] for i in self.nagitems_filtered["hosts"].values(): for h in i: new_nagitems_filtered_list.append((h.name, h.status)) for i in self.nagitems_filtered["services"].values(): for s in i: new_nagitems_filtered_list.append((s.host, s.name, s.status)) # sort for better comparison new_nagitems_filtered_list.sort() # if both lists are identical there was no status change if (self.nagitems_filtered_list == new_nagitems_filtered_list): self.WorstStatus = "UP" else: # if the new list is shorter than the first and there are no different hosts # there one host/service must have been recovered, which is not worth a notification diff = [] for i in new_nagitems_filtered_list: if not i in self.nagitems_filtered_list: # collect differences diff.append(i) if len(diff) == 0: self.WorstStatus = "UP" else: # if there are different hosts/services in list of new hosts there must be a notification # get list of states for comparison diff_states = [] for d in diff: diff_states.append(d[-1]) # temporary worst state index worst = 0 for d in diff_states: # only check worst state if it is valid if d in self.States: if self.States.index(d) > worst: worst = self.States.index(d) # final worst state is one of the predefined states self.WorstStatus = self.States[worst] # copy of listed nagitems for next comparison self.nagitems_filtered_list = copy.deepcopy(new_nagitems_filtered_list) del new_nagitems_filtered_list # put new informations into respective dictionaries self.hosts = copy.deepcopy(self.new_hosts) self.new_hosts.clear() # after all checks are done unset checking flag self.isChecking = False # return True if all worked well return Result() def FetchURL(self, url, giveback="obj", cgi_data=None, no_auth=False): """ get content of given url, cgi_data only used if present "obj" FetchURL gives back a dict full of miserable hosts/services, "xml" giving back as objectified xml "raw" it gives back pure HTML - useful for finding out IP or new version existence of cgi_data forces urllib to use POST instead of GET requests NEW: gives back a list containing result and, if necessary, a more clear error description """ # run this method which checks itself if there is some action to take for initializing connection # if no_auth is true do not use Auth headers, used by Actions.CheckForNewVersion() if no_auth == False: self.init_HTTP() # to avoid race condition and credentials leak use local HTTPheaders HTTPheaders = self.HTTPheaders else: HTTPheaders = dict() HTTPheaders["raw"] = HTTPheaders["obj"] = HTTPheaders["xml"] = dict() try: try: # debug if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="FetchURL: " + url + " CGI Data: " + str(cgi_data)) request = urllib2.Request(url, cgi_data, HTTPheaders[giveback]) # use opener - if cgi_data is not empty urllib uses a POST request urlcontent = self.urlopener.open(request) del url, cgi_data, request except: del url, cgi_data, request result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # give back pure HTML or XML in case giveback is "raw" if giveback == "raw": result = Result(result=urlcontent.read().decode("utf8", errors="ignore")) urlcontent.close() del urlcontent return result # objectified HTML if giveback == "obj": yummysoup = BeautifulSoup(urlcontent.read().decode("utf8", errors="ignore"), convertEntities=BeautifulSoup.ALL_ENTITIES) urlcontent.close() del urlcontent #return Result(result=copy.deepcopy(yummysoup)) return Result(result=yummysoup) # objectified generic XML, valid at least for Opsview and Centreon elif giveback == "xml": xmlobj = BeautifulStoneSoup(urlcontent.read().decode("utf8", errors="ignore"), convertEntities=BeautifulStoneSoup.XML_ENTITIES) urlcontent.close() del urlcontent #return Result(result=copy.deepcopy(xmlobj)) return Result(result=xmlobj) except: # do some cleanup result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) def GetHost(self, host): """ find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Nagios """ # the fasted method is taking hostname as used in monitor if str(self.conf.connect_by_host) == "True" or host == "": return Result(result=host) # initialize ip string ip = "" # glue nagios cgi url and hostinfo cgiurl_host = self.monitor_cgi_url + "/extinfo.cgi?type=1&host=" + host # get host info result = self.FetchURL(cgiurl_host, giveback="obj") htobj = result.result try: # take ip from html soup ip = htobj.findAll(name="div", attrs={"class":"data"})[-1].text # workaround for URL-ified IP as described in SF bug 2967416 # https://sourceforge.net/tracker/?func=detail&aid=2967416&group_id=236865&atid=1101370 if "://" in ip: ip = ip.split("://")[1] # last-minute-workaround for https://github.com/HenriWahl/Nagstamon/issues/48 if "," in ip: ip = ip.split(",")[0] # print IP in debug mode if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug ="IP of %s:" % (host) + " " + ip) # when connection by DNS is not configured do it by IP if str(self.conf.connect_by_dns) == "True": # try to get DNS name for ip, if not available use ip try: address = socket.gethostbyaddr(ip)[0] except: address = ip else: address = ip except: result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # do some cleanup del htobj # give back host or ip return Result(result=address) def Hook(self): """ allows to add some extra actions for a monitor server to be executed in RefreshLoop inspired by Centreon and its seemingly Alzheimer desease regarding session ID/Cookie/whatever """ pass def Error(self, error): """ Handle errors somehow - print them or later log them into not yet existing log file """ if str(self.conf.debug_mode) == "True": debug = "" for line in traceback.format_exception(error[0], error[1], error[2], 5): debug += line self.Debug(server=self.get_name(), debug=debug, head="ERROR") return ["ERROR", traceback.format_exception_only(error[0], error[1])[0]] def Debug(self, server="", host="", service="", debug="", head="DEBUG"): """ centralized debugging """ debug_string = " ".join((head + ":", str(datetime.datetime.now()), server, host, service, debug)) # give debug info to debug loop for thread-save log-file writing self.debug_queue.put(debug_string) Nagstamon/Nagstamon/Server/Icinga.py000066400000000000000000000731361240775040100177600ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 from Nagstamon.Server.Generic import GenericServer import urllib import sys import copy # this seems to be necessary for json to be packaged by pyinstaller from encodings import hex_codec import json import base64 # to let Linux distributions use their own BeautifulSoup if existent try importing local BeautifulSoup first # see https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3302612&group_id=236865 try: from BeautifulSoup import BeautifulSoup except: from Nagstamon.thirdparty.BeautifulSoup import BeautifulSoup from Nagstamon.Objects import * from Nagstamon.Actions import * class IcingaServer(GenericServer): """ object of Incinga server """ TYPE = 'Icinga' # flag to handle JSON or HTML correctly - checked by get_server_version() json = None # autologin is used only by Centreon DISABLED_CONTROLS = ["input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key"] def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # dummy default empty cgi urls - get filled later when server version is known self.cgiurl_services = None self.cgiurl_hosts = None def init_HTTP(self): """ Icinga 1.11 needs extra Referer header for actions """ GenericServer.init_HTTP(self) if not "Referer" in self.HTTPheaders: # to execute actions since Icinga 1.11 a Referer Header is necessary for giveback in ["raw", "obj"]: self.HTTPheaders[giveback]["Referer"] = self.monitor_cgi_url + "/cmd.cgi" def get_server_version(self): """ Try to get Icinga version for different URLs and JSON capabilities """ result = self.FetchURL("%s/tac.cgi?jsonoutput" % (self.monitor_cgi_url), giveback="raw") if result.error != "": return result else: tacraw = result.result if tacraw.startswith("<"): self.json = False tacsoup = BeautifulSoup(tacraw) self.version = tacsoup.find("a", { "class" : "homepageURL" }) # only extract version if HTML seemed to be OK if self.version.__dict__.has_key("contents"): self.version = self.version.contents[0].split("Icinga ")[1] elif tacraw.startswith("{"): # there seem to be problems with Icinga < 1.6 # in case JSON parsing crashes fall back to HTML try: jsondict = json.loads(tacraw) self.version = jsondict["cgi_json_version"] self.json = True except: self.version = "1.6" self.json = False def _get_status(self): """ Get status from Icinga Server, prefer JSON if possible """ try: if self.json == None: # we need to get the server version and its JSONability result = self.get_server_version() if self.version != "": # define CGI URLs for hosts and services depending on JSON-capable server version if self.cgiurl_hosts == self.cgiurl_services == None: if self.version < "1.7": # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes # services (unknown, warning or critical?) as dictionary, sorted by hard and soft state type self.cgiurl_services = {"hard": self.monitor_cgi_url + "/status.cgi?host=all&servicestatustypes=253&serviceprops=262144",\ "soft": self.monitor_cgi_url + "/status.cgi?host=all&servicestatustypes=253&serviceprops=524288"} # hosts (up or down or unreachable) self.cgiurl_hosts = {"hard": self.monitor_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=262144",\ "soft": self.monitor_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=524288"} else: # services (unknown, warning or critical?) self.cgiurl_services = {"hard": self.monitor_cgi_url + "/status.cgi?style=servicedetail&servicestatustypes=253&serviceprops=262144",\ "soft": self.monitor_cgi_url + "/status.cgi?style=servicedetail&servicestatustypes=253&serviceprops=524288"} # hosts (up or down or unreachable) self.cgiurl_hosts = {"hard": self.monitor_cgi_url + "/status.cgi?style=hostdetail&hoststatustypes=12&hostprops=262144",\ "soft": self.monitor_cgi_url + "/status.cgi?style=hostdetail&hoststatustypes=12&hostprops=524288"} if self.json == True: for status_type in "hard", "soft": self.cgiurl_services[status_type] += "&jsonoutput" self.cgiurl_hosts[status_type] += "&jsonoutput" # get status depending on JSONablility if self.json == True: self._get_status_JSON() else: self._get_status_HTML() else: # error result in case version still was "" return result except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) #dummy return in case all is OK return Result() def _get_status_JSON(self): """ Get status from Icinga Server - the JSON way """ # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # now using JSON output from Icinga try: for status_type in "hard", "soft": result = self.FetchURL(self.cgiurl_hosts[status_type], giveback="raw") # purify JSON result of unnecessary control sequence \n jsonraw, error = copy.deepcopy(result.result.replace("\n", "")), copy.deepcopy(result.error) if error != "": return Result(result=jsonraw, error=error) jsondict = json.loads(jsonraw) hosts = copy.deepcopy(jsondict["status"]["host_status"]) for host in hosts: # make dict of tuples for better reading h = dict(host.items()) # host if str(self.use_display_name_host) == "False": # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be host_name instead of host_display_name # legacy Icinga adjustments if h.has_key("host_name"): host_name = h["host_name"] elif h.has_key("host"): host_name = h["host"] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = h["host_display_name"] # host objects contain service objects if not self.new_hosts.has_key(host_name): self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].server = self.name self.new_hosts[host_name].status = h["status"] self.new_hosts[host_name].last_check = h["last_check"] self.new_hosts[host_name].duration = h["duration"] self.new_hosts[host_name].attempt = h["attempts"] self.new_hosts[host_name].status_information= h["status_information"].encode("utf-8").replace("\n", " ").strip() self.new_hosts[host_name].passiveonly = not(h["active_checks_enabled"]) self.new_hosts[host_name].notifications_disabled = not(h["notifications_enabled"]) self.new_hosts[host_name].flapping = h["is_flapping"] self.new_hosts[host_name].acknowledged = h["has_been_acknowledged"] self.new_hosts[host_name].scheduled_downtime = h["in_scheduled_downtime"] self.new_hosts[host_name].status_type = status_type del h, host_name except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in "hard", "soft": result = self.FetchURL(self.cgiurl_services[status_type], giveback="raw") # purify JSON result of unnecessary control sequence \n jsonraw, error = copy.deepcopy(result.result.replace("\n", "")), copy.deepcopy(result.error) if error != "": return Result(result=jsonraw, error=error) jsondict = json.loads(jsonraw) services = copy.deepcopy(jsondict["status"]["service_status"]) for service in services: # make dict of tuples for better reading s = dict(service.items()) if str(self.use_display_name_host) == "False": # according to http://sourceforge.net/p/nagstamon/bugs/83/ it might # better be host_name instead of host_display_name # legacy Icinga adjustments if s.has_key("host_name"): host_name = s["host_name"] elif s.has_key("host"): host_name = s["host"] else: # https://github.com/HenriWahl/Nagstamon/issues/46 on the other hand has # problems with that so here we go with extra display_name option host_name = s["host_display_name"] # host objects contain service objects if not self.new_hosts.has_key(host_name): self.new_hosts[host_name] = GenericHost() self.new_hosts[host_name].name = host_name self.new_hosts[host_name].status = "UP" if str(self.use_display_name_host) == "False": # legacy Icinga adjustments if s.has_key("service_description"): service_name = s["service_description"] elif s.has_key("description"): service_name = s["description"] elif s.has_key("service"): service_name = s["service"] else: service_name = s["service_display_name"] # if a service does not exist create its object if not self.new_hosts[host_name].services.has_key(service_name): self.new_hosts[host_name].services[service_name] = GenericService() self.new_hosts[host_name].services[service_name].host = host_name self.new_hosts[host_name].services[service_name].name = service_name self.new_hosts[host_name].services[service_name].server = self.name self.new_hosts[host_name].services[service_name].status = s["status"] self.new_hosts[host_name].services[service_name].last_check = s["last_check"] self.new_hosts[host_name].services[service_name].duration = s["duration"] self.new_hosts[host_name].services[service_name].attempt = s["attempts"] self.new_hosts[host_name].services[service_name].status_information = s["status_information"].encode("utf-8").replace("\n", " ").strip() self.new_hosts[host_name].services[service_name].passiveonly = not(s["active_checks_enabled"]) self.new_hosts[host_name].services[service_name].notifications_disabled = not(s["notifications_enabled"]) self.new_hosts[host_name].services[service_name].flapping = s["is_flapping"] self.new_hosts[host_name].services[service_name].acknowledged = s["has_been_acknowledged"] self.new_hosts[host_name].services[service_name].scheduled_downtime = s["in_scheduled_downtime"] self.new_hosts[host_name].services[service_name].status_type = status_type del s, host_name, service_name except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del jsonraw, jsondict, error, hosts, services #dummy return in case all is OK return Result() def _get_status_HTML(self): """ Get status from Nagios Server - the oldschool CGI HTML way """ # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily ###global icons nagitems = {"services":[], "hosts":[]} # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: for status_type in "hard", "soft": result = self.FetchURL(self.cgiurl_hosts[status_type]) htobj, error = result.result, result.error if error != "": return Result(result=htobj, error=error) # put a copy of a part of htobj into table to be able to delete htobj table = htobj('table', {'class': 'status'})[0] # do some cleanup del result, error # access table rows # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) # kick out table heads trs.pop(0) for tr in trs: try: # ignore empty rows if len(tr('td', recursive=False)) > 1: n = {} # get tds in one tr tds = tr('td', recursive=False) # host try: n["host"] = str(tds[0].table.tr.td.table.tr.td.a.string) except: n["host"] = str(nagitems[len(nagitems)-1]["host"]) # status n["status"] = str(tds[1].string) # last_check n["last_check"] = str(tds[2].string) # duration n["duration"] = str(tds[3].string) # division between Nagios and Icinga in real life... where # Nagios has only 5 columns there are 7 in Icinga 1.3... # ... and 6 in Icinga 1.2 :-) if len(tds) < 7: # the old Nagios table # status_information if len(tds[4](text=not_empty)) == 0: n["status_information"] = "" else: n["status_information"] = str(tds[4].string).encode("utf-8") # attempts are not shown in case of hosts so it defaults to "N/A" n["attempt"] = "N/A" else: # attempts are shown for hosts # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n["attempt"] = str(tds[4].string).strip() # status_information if len(tds[5](text=not_empty)) == 0: n["status_information"] = "" else: n["status_information"] = str(tds[5].string).encode("utf-8") # status flags n["passiveonly"] = False n["notifications_disabled"] = False n["flapping"] = False n["acknowledged"] = False n["scheduled_downtime"] = False # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i["src"].split("/")[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this host item to nagitems nagitems["hosts"].append(n) # after collection data in nagitems create objects from its informations # host objects contain service objects if not self.new_hosts.has_key(n["host"]): new_host = n["host"] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n["host"] self.new_hosts[new_host].server = self.name self.new_hosts[new_host].status = n["status"] self.new_hosts[new_host].last_check = n["last_check"] self.new_hosts[new_host].duration = n["duration"] self.new_hosts[new_host].attempt = n["attempt"] self.new_hosts[new_host].status_information= n["status_information"].encode("utf-8").replace("\n", " ").strip() self.new_hosts[new_host].passiveonly = n["passiveonly"] self.new_hosts[new_host].notifications_disabled = n["notifications_disabled"] self.new_hosts[new_host].flapping = n["flapping"] self.new_hosts[new_host].acknowledged = n["acknowledged"] self.new_hosts[new_host].scheduled_downtime = n["scheduled_downtime"] self.new_hosts[new_host].status_type = status_type # some cleanup del tds, n except: self.Error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # services try: for status_type in "hard", "soft": result = self.FetchURL(self.cgiurl_services[status_type]) htobj, error = result.result, result.error #if error != "": return Result(result=copy.deepcopy(htobj), error=error) if error != "": return Result(result=htobj, error=error) table = htobj('table', {'class': 'status'})[0] # some Icinga versions have a tag in cgi output HTML which # omits the tags being found if len(table('tbody')) == 0: trs = table('tr', recursive=False) else: tbody = table('tbody')[0] trs = tbody('tr', recursive=False) # do some cleanup del result, error # kick out table heads trs.pop(0) for tr in trs: try: # ignore empty rows - there are a lot of them - a Nagios bug? tds = tr('td', recursive=False) if len(tds) > 1: n = {} # host # the resulting table of Nagios status.cgi table omits the # hostname of a failing service if there are more than one # so if the hostname is empty the nagios status item should get # its hostname from the previuos item - one reason to keep "nagitems" try: n["host"] = str(tds[0](text=not_empty)[0]) except: n["host"] = str(nagitems["services"][len(nagitems["services"])-1]["host"]) # service n["service"] = str(tds[1](text=not_empty)[0]) # status n["status"] = str(tds[2](text=not_empty)[0]) # last_check n["last_check"] = str(tds[3](text=not_empty)[0]) # duration n["duration"] = str(tds[4](text=not_empty)[0]) # attempt # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs # to be stripped n["attempt"] = str(tds[5](text=not_empty)[0]).strip() # status_information if len(tds[6](text=not_empty)) == 0: n["status_information"] = "" else: n["status_information"] = str(tds[6](text=not_empty)[0]).encode("utf-8") # status flags n["passiveonly"] = False n["notifications_disabled"] = False n["flapping"] = False n["acknowledged"] = False n["scheduled_downtime"] = False # map status icons to status flags icons = tds[1].findAll('img') for i in icons: icon = i["src"].split("/")[-1] if icon in self.STATUS_MAPPING: n[self.STATUS_MAPPING[icon]] = True # cleaning del icons # add dictionary full of information about this service item to nagitems - only if service nagitems["services"].append(n) # after collection data in nagitems create objects of its informations # host objects contain service objects if not self.new_hosts.has_key(n["host"]): self.new_hosts[n["host"]] = GenericHost() self.new_hosts[n["host"]].name = n["host"] self.new_hosts[n["host"]].status = "UP" # trying to fix https://sourceforge.net/tracker/index.php?func=detail&aid=3299790&group_id=236865&atid=1101370 # if host is not down but in downtime or any other flag this should be evaluated too # map status icons to status flags icons = tds[0].findAll('img') for i in icons: icon = i["src"].split("/")[-1] if icon in self.STATUS_MAPPING: self.new_hosts[n["host"]].__dict__[self.STATUS_MAPPING[icon]] = True # cleaning del icons # if a service does not exist create its object if not self.new_hosts[n["host"]].services.has_key(n["service"]): new_service = n["service"] self.new_hosts[n["host"]].services[new_service] = GenericService() self.new_hosts[n["host"]].services[new_service].host = n["host"] self.new_hosts[n["host"]].services[new_service].server = self.name self.new_hosts[n["host"]].services[new_service].name = n["service"] self.new_hosts[n["host"]].services[new_service].status = n["status"] self.new_hosts[n["host"]].services[new_service].last_check = n["last_check"] self.new_hosts[n["host"]].services[new_service].duration = n["duration"] self.new_hosts[n["host"]].services[new_service].attempt = n["attempt"] self.new_hosts[n["host"]].services[new_service].status_information = n["status_information"].encode("utf-8").replace("\n", " ").strip() self.new_hosts[n["host"]].services[new_service].passiveonly = n["passiveonly"] self.new_hosts[n["host"]].services[new_service].notifications_disabled = n["notifications_disabled"] self.new_hosts[n["host"]].services[new_service].flapping = n["flapping"] self.new_hosts[n["host"]].services[new_service].acknowledged = n["acknowledged"] self.new_hosts[n["host"]].services[new_service].scheduled_downtime = n["scheduled_downtime"] # some cleanup del tds, n except: self.Error(sys.exc_info()) # do some cleanup htobj.decompose() del htobj, trs, table except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # some cleanup del nagitems #dummy return in case all is OK return Result() def _set_recheck(self, host, service): """ to solve https://sourceforge.net/p/nagstamon/feature-requests/74/ there is a comment parameter added to cgi request """ if service != "": if self.hosts[host].services[service].is_passive_only(): # Do not check passive only checks return # get start time from Nagios as HTML to use same timezone setting like the locally installed Nagios result = self.FetchURL(self.monitor_cgi_url + "/cmd.cgi?" + urllib.urlencode({"cmd_typ":"96", "host":host})) self.start_time = dict(result.result.find(attrs={"name":"start_time"}).attrs)["value"] # decision about host or service - they have different URLs if service == "": # host cmd_typ = "96" else: # service @ host cmd_typ = "7" # ignore empty service in case of rechecking a host cgi_data = urllib.urlencode([("cmd_typ", cmd_typ),\ ("cmd_mod", "2"),\ ("host", host),\ ("service", service),\ ("start_time", self.start_time),\ ("force_check", "on"),\ ("com_data", "Recheck by %s" % self.username),\ ("btnSubmit", "Commit")]) # execute POST request self.FetchURL(self.monitor_cgi_url + "/cmd.cgi", giveback="raw", cgi_data=cgi_data) Nagstamon/Nagstamon/Server/Multisite.py000066400000000000000000000630651240775040100205450ustar00rootroot00000000000000# -*- encoding: utf-8; py-indent-offset: 4 -*- # +------------------------------------------------------------------+ # | ____ _ _ __ __ _ __ | # | / ___| |__ ___ ___| | __ | \/ | |/ / | # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / | # | | |___| | | | __/ (__| < | | | | . \ | # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ | # | | # | Copyright Mathias Kettner 2010 mk@mathias-kettner.de | # | lm@mathias-kettner.de | # +------------------------------------------------------------------+ # # The official homepage is at http://mathias-kettner.de/check_mk. # # check_mk 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 in version 2. check_mk is distributed # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with- # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. See the GNU General Public License for more de- # ails. You should have received a copy of the GNU General Public # License along with GNU Make; see the file COPYING. If not, write # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301 USA. # hax0rized by: lm@mathias-kettner.de import sys import urllib import webbrowser import time import copy from Nagstamon import Actions from Nagstamon.Objects import * from Nagstamon.Server.Generic import GenericServer class MultisiteError(Exception): def __init__(self, terminate, result): self.terminate = terminate self.result = result class LastCheckColumnMultisite(Column): """ because Check_MK has a pretty different date format (much better readable) it has to be treaten differently This is a custom version of LastCheckColumn to be used in list COLUMNS in class Multisite """ ATTR_NAME = 'last_check' @classmethod def sort_function(cls, model, iter1, iter2, column): """ Overrides default sorting behaviour """ data1, data2 = [model.get_value(x, column) for x in (iter1, iter2)] try: first = Actions.MachineSortableDateMultisite(data1) second = Actions.MachineSortableDateMultisite(data2) except ValueError, err: print err return cmp(first, second) # other order than default function return second - first class DurationColumnMultisite(CustomSortingColumn): ATTR_NAME = 'duration' @classmethod def sort_function(cls, model, iter1, iter2, column): """ Overrides default sorting behaviour """ data1, data2 = [model.get_value(x, column) for x in (iter1, iter2)] try: first = Actions.MachineSortableDateMultisite(data1) second = Actions.MachineSortableDateMultisite(data2) except ValueError, err: print err return cmp(first, second) # other order than default function return second - first class MultisiteServer(GenericServer): """ special treatment for Check_MK Multisite JSON API """ TYPE = 'Check_MK Multisite' # URLs for browser shortlinks/buttons on popup window BROWSER_URLS= { "monitor": "$MONITOR$",\ "hosts": "$MONITOR$/index.py?start_url=view.py?view_name=hostproblems",\ "services": "$MONITOR$/index.py?start_url=view.py?view_name=svcproblems",\ "history": '$MONITOR$/index.py?start_url=view.py?view_name=events'} # A Monitor CGI URL is not necessary so hide it in settings # autologin is used only by Centreon DISABLED_CONTROLS = ["label_monitor_cgi_url", "input_entry_monitor_cgi_url", "input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] COLUMNS = [ HostColumn, ServiceColumn, StatusColumn, LastCheckColumnMultisite, DurationColumnMultisite, AttemptColumn, StatusInformationColumn ] def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Prepare all urls needed by nagstamon - self.urls = {} self.statemap = {} # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Downtime"] # flag for newer cookie authentication self.CookieAuth = False def init_HTTP(self): # Fix eventually missing tailing "/" in url if self.monitor_url[-1] != '/': self.monitor_url += '/' # Prepare all urls needed by nagstamon if not yet done if len(self.urls) == len(self.statemap): self.urls = { 'api_services': self.monitor_url + "view.py?view_name=nagstamon_svc&output_format=python&lang=&limit=hard", 'human_services': self.monitor_url + "index.py?%s" % \ urllib.urlencode({'start_url': 'view.py?view_name=nagstamon_svc'}), 'human_service': self.monitor_url + "index.py?%s" % urllib.urlencode({'start_url': 'view.py?view_name=service'}), 'api_hosts': self.monitor_url + "view.py?view_name=nagstamon_hosts&output_format=python&lang=&limit=hard", 'human_hosts': self.monitor_url + "index.py?%s" % urllib.urlencode({'start_url': 'view.py?view_name=nagstamon_hosts'}), 'human_host': self.monitor_url + "index.py?%s" % urllib.urlencode({'start_url': 'view.py?view_name=hoststatus'}), # URLs do not need pythonic output because since werk #0766 API does not work with transid=-1 anymore # thus access to normal webinterface is used #'api_host_act': self.monitor_url + 'view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=hoststatus&filled_in=actions&lang=', #'api_service_act': self.monitor_url + 'view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=service&filled_in=actions&lang=', #'api_svcprob_act': self.monitor_url + 'view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&output_format=python&view_name=svcproblems&filled_in=actions&lang=', 'api_host_act': self.monitor_url + 'view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&view_name=hoststatus&filled_in=actions&lang=', 'api_service_act': self.monitor_url + 'view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&view_name=service&filled_in=actions&lang=', 'api_svcprob_act': self.monitor_url + 'view.py?_transid=-1&_do_actions=yes&_do_confirm=Yes!&view_name=svcproblems&filled_in=actions&lang=', 'human_events': self.monitor_url + "index.py?%s" % urllib.urlencode({'start_url': 'view.py?view_name=events'}), 'togglevisibility':self.monitor_url + "user_profile.py", 'transid': self.monitor_url + "view.py?actions=yes&filled_in=actions&host=$HOST$&service=$SERVICE$&view_name=service" } self.statemap = { 'UNREACH': 'UNREACHABLE', 'CRIT': 'CRITICAL', 'WARN': 'WARNING', 'UNKN': 'UNKNOWN', 'PEND': 'PENDING', } if self.CookieAuth: # get cookie to access Check_MK web interface if len(self.Cookie) == 0: # if no cookie yet login self._get_cookie_login() GenericServer.init_HTTP(self) def init_config(self): """ dummy init_config, called at thread start, not really needed here, just omit extra properties """ pass def _get_url(self, url): result = self.FetchURL(url, 'raw') content, error = result.result, result.error if error != "": #raise MultisiteError(True, Result(result = copy.deepcopy(content), error = error)) raise MultisiteError(True, Result(result = content, error = error)) if content.startswith('WARNING:'): c = content.split("\n") # Print non ERRORS to the log in debug mode self.Debug(server=self.get_name(), debug=c[0]) raise MultisiteError(False, Result(result = "\n".join(c[1:]), content = eval("\n".join(c[1:])), error = c[0])) elif content.startswith('ERROR:'): raise MultisiteError(True, Result(result = content, error = content)) # in case of auth problem enable GUI auth part in popup if self.CookieAuth == True and len(self.Cookie) == 0: self.refresh_authentication = True return Result(result="", error="Authentication failed") # looks like cookieauth elif content.startswith('<'): self.CookieAuth = True # if first attempt login and then try to get data again if len(self.Cookie) == 0: self._get_cookie_login() result = self.FetchURL(url, 'raw') content, error = result.result, result.error if content.startswith('<'): return "" return eval(content) def _get_cookie_login(self): """ login on cookie monitor site """ # put all necessary data into url string logindata = urllib.urlencode({"_username":self.get_username(),\ "_password":self.get_password(),\ "_login":"1",\ "_origtarget": "",\ "filled_in":"login"}) # get cookie from login page via url retrieving as with other urls try: # login and get cookie urlcontent = self.urlopener.open(self.monitor_url + "/login.py", logindata) urlcontent.close() except: self.Error(sys.exc_info()) def _get_status(self): """ Get status from Check_MK Server """ ret = Result() # Create URLs for the configured filters url_params = '' url_params += '&is_host_acknowledged=-1&is_service_acknowledged=-1' url_params += '&is_host_notifications_enabled=-1&is_service_notifications_enabled=-1' url_params += '&is_host_active_checks_enabled=-1&is_service_active_checks_enabled=-1' url_params += '&host_scheduled_downtime_depth=-1&is_in_downtime=-1' try: response = [] try: response = self._get_url(self.urls['api_hosts'] + url_params) except MultisiteError, e: if e.terminate: return e.result for row in response[1:]: host= dict(zip(copy.deepcopy(response[0]), copy.deepcopy(row))) n = { 'host': host['host'], 'status': self.statemap.get(host['host_state'], host['host_state']), 'last_check': host['host_check_age'], 'duration': host['host_state_age'], 'status_information': host['host_plugin_output'], 'attempt': host['host_attempt'], 'site': host['sitename_plain'], 'address': host['host_address'], } # host objects contain service objects if not self.new_hosts.has_key(n["host"]): new_host = n["host"] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n["host"] self.new_hosts[new_host].server = self.name self.new_hosts[new_host].status = n["status"] self.new_hosts[new_host].last_check = n["last_check"] self.new_hosts[new_host].duration = n["duration"] self.new_hosts[new_host].attempt = n["attempt"] self.new_hosts[new_host].status_information= n["status_information"].replace("\n", " ") self.new_hosts[new_host].site = n["site"] self.new_hosts[new_host].address = n["address"] # transisition to Check_MK 1.1.10p2 if host.has_key('host_in_downtime'): if host['host_in_downtime'] == 'yes': self.new_hosts[new_host].scheduled_downtime = True if host.has_key('host_acknowledged'): if host['host_acknowledged'] == 'yes': self.new_hosts[new_host].acknowledged = True # hard/soft state for later filter evaluation real_attempt, max_attempt = self.new_hosts[new_host].attempt.split("/") if real_attempt <> max_attempt: self.new_hosts[new_host].status_type = "soft" else: self.new_hosts[new_host].status_type = "hard" del response except: self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # Add filters to the url which should only be applied to the service request if str(self.conf.filter_services_on_unreachable_hosts) == "True": url_params += '&hst2=0' # services try: response = [] try: response = self._get_url(self.urls['api_services'] + url_params) except MultisiteError, e: if e.terminate: return e.result else: response = copy.deepcopy(e.result.content) ret = copy.deepcopy(e.result) for row in response[1:]: service = dict(zip(copy.deepcopy(response[0]), copy.deepcopy(row))) n = { 'host': service['host'].encode("utf-8"), 'service': service['service_description'].encode("utf-8"), 'status': self.statemap.get(service['service_state'], service['service_state']), 'last_check': service['svc_check_age'], 'duration': service['svc_state_age'], 'attempt': service['svc_attempt'], 'status_information': service['svc_plugin_output'].encode("utf-8"), # Check_MK passive services can be re-scheduled by using the Check_MK service 'passiveonly': service['svc_is_active'] == 'no' and not service['svc_check_command'].startswith('check_mk'), 'notifications': service['svc_notifications_enabled'] == 'yes', 'flapping': service['svc_flapping'] == 'yes', 'site': service['sitename_plain'], 'address': service['host_address'], 'command': service['svc_check_command'], } # host objects contain service objects if not self.new_hosts.has_key(n["host"]): self.new_hosts[n["host"]] = GenericHost() self.new_hosts[n["host"]].name = n["host"] self.new_hosts[n["host"]].status = "UP" self.new_hosts[n["host"]].site = n["site"] self.new_hosts[n["host"]].address = n["address"] # if a service does not exist create its object if not self.new_hosts[n["host"]].services.has_key(n["service"]): new_service = n["service"] self.new_hosts[n["host"]].services[new_service] = GenericService() self.new_hosts[n["host"]].services[new_service].host = n["host"] self.new_hosts[n["host"]].services[new_service].server = self.name self.new_hosts[n["host"]].services[new_service].name = n["service"] self.new_hosts[n["host"]].services[new_service].status = n["status"] self.new_hosts[n["host"]].services[new_service].last_check = n["last_check"] self.new_hosts[n["host"]].services[new_service].duration = n["duration"] self.new_hosts[n["host"]].services[new_service].attempt = n["attempt"] self.new_hosts[n["host"]].services[new_service].status_information = n["status_information"].replace("\n", " ").strip() self.new_hosts[n["host"]].services[new_service].passiveonly = n["passiveonly"] self.new_hosts[n["host"]].services[new_service].flapping = n["flapping"] self.new_hosts[n["host"]].services[new_service].site = n["site"] self.new_hosts[n["host"]].services[new_service].address = n["address"] self.new_hosts[n["host"]].services[new_service].command = n["command"] # transistion to Check_MK 1.1.10p2 if service.has_key('svc_in_downtime'): if service['svc_in_downtime'] == 'yes': self.new_hosts[n["host"]].services[new_service].scheduled_downtime = True if service.has_key('svc_acknowledged'): if service['svc_acknowledged'] == 'yes': self.new_hosts[n["host"]].services[new_service].acknowledged = True if service.has_key('svc_flapping'): if service['svc_flapping'] == 'yes': self.new_hosts[n["host"]].services[new_service].flapping = True # hard/soft state for later filter evaluation real_attempt, max_attempt = self.new_hosts[n["host"]].services[new_service].attempt.split("/") if real_attempt <> max_attempt: self.new_hosts[n["host"]].services[new_service].status_type = "soft" else: self.new_hosts[n["host"]].services[new_service].status_type = "hard" del response except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=copy.deepcopy(result), error=copy.deepcopy(error)) del url_params return ret def open_tree_view(self, host, service=""): """ open monitor from treeview context menu """ if service == "": url = self.urls['human_host'] + urllib.urlencode({'x': 'site='+self.hosts[host].site+'&host='+host}).replace('x=', '%26') else: url = self.urls['human_service'] + urllib.urlencode({'x': 'site='+self.hosts[host].site+'&host='+host+'&service='+service}).replace('x=', '%26') if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, service=service, debug="Open host/service monitor web page " + url) webbrowser.open(url) def GetHost(self, host): """ find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Nagios """ # the fastest method is taking hostname as used in monitor if str(self.conf.connect_by_host) == "True" or host == "": return Result(result=host) ip = "" try: if host in self.hosts: ip = self.hosts[host].address if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug ="IP of %s:" % (host) + " " + ip) if str(self.conf.connect_by_dns) == "True": try: address = socket.gethostbyaddr(ip)[0] except: address = ip else: address = ip except: result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) return Result(result=address) def get_start_end(self, host): return time.strftime("%Y-%m-%d %H:%M"), time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() + 7200)) def _action(self, site, host, service, specific_params): params = { 'site': self.hosts[host].site, 'host': host, } params.update(specific_params) # service is now added in Actions.Action(): action_type "url-check_mk-multisite" if service != "": url = self.urls['api_service_act'] else: url = self.urls['api_host_act'] if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug ="Submitting action: " + url + '&' + urllib.urlencode(params)) action = Actions.Action(type="url-check_mk-multisite",\ string=url + '&' + urllib.urlencode(params),\ conf=self.conf,\ host = host,\ service = service,\ server=self) action.run() def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): self._action(self.hosts[host].site, host, service, { '_down_comment': author == self.username and comment or '%s: %s' % (author, comment), '_down_flexible': fixed == 0 and 'on' or '', '_down_custom': 'Custom+time+range', '_down_from_date': start_time.split(' ')[0], '_down_from_time': start_time.split(' ')[1], '_down_to_date': end_time.split(' ')[0], '_down_to_time': end_time.split(' ')[1], '_down_duration': '%s:%s' % (hours, minutes), }) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=[]): p = { '_acknowledge': 'Acknowledge', '_ack_sticky': sticky == 1 and 'on' or '', '_ack_notify': notify == 1 and 'on' or '', '_ack_persistent': persistent == 1 and 'on' or '', '_ack_comment': author == self.username and comment or '%s: %s' % (author, comment) } self._action(self.hosts[host].site, host, service, p) # acknowledge all services on a host when told to do so for s in all_services: self._action(self.hosts[host].site, host, s, p) def _set_recheck(self, host, service): p = { '_resched_checks': 'Reschedule active checks', } self._action(self.hosts[host].site, host, service, p) def recheck_all(self): """ special method for Check_MK as there is one URL for rescheduling all problems to be checked """ params = dict() params['_resched_checks'] = 'Reschedule active checks' url = self.urls['api_svcprob_act'] if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug ="Rechecking all action: " + url + '&' + urllib.urlencode(params)) result = self.FetchURL(url + '&' + urllib.urlencode(params), giveback = 'raw') """ def ToggleVisibility(self, widget): #Attempt to enable/disable visibility of all problems for user via #/user_profile.py?cb_ua_force_authuser=0&cb_ua_force_authuser_webservice=0&filled_in=profile # since werk #0766 http://mathias-kettner.de/check_mk_werks.php?werk_id=766 a real transid is needed transid = self.FetchURL(self.urls["togglevisibility"], "obj").result.find(attrs={"name" : "_transid"})["value"] cgi_data = urllib.urlencode({"cb_ua_force_authuser" : str(int(widget.get_active())),\ "cb_ua_force_authuser_webservice" : str(int(widget.get_active())),\ "filled_in" : "profile",\ "_transid" : transid,\ "_save" : "Save"}) self.FetchURL(self.urls["togglevisibility"], "raw", cgi_data=urllib.urlencode(\ {"cb_ua_force_authuser" : str(int(widget.get_active())),\ "cb_ua_force_authuser_webservice" : str(int(widget.get_active())),\ "filled_in" : "profile",\ "_transid" : transid,\ "_save" : "Save"})).result """ def _get_transid(self, host, service): """ get transid for an action """ transid = self.FetchURL(self.urls["transid"].replace("$HOST$", host).replace("$SERVICE$", service.replace(" ", "+")),\ "obj").result.find(attrs={"name" : "_transid"})["value"] return transid Nagstamon/Nagstamon/Server/Nagios.py000066400000000000000000000027511240775040100200010ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 from Nagstamon.Server.Generic import GenericServer class NagiosServer(GenericServer): """ object of Nagios server - when nagstamon will be able to poll various servers this will be useful As Nagios is the default server type all its methods are in GenericServer """ TYPE = 'Nagios' # autologin is used only by Centreon DISABLED_CONTROLS = ["input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] Nagstamon/Nagstamon/Server/Ninja.py000066400000000000000000000324111240775040100176140ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # Large parts of Ninja support copyright by Op5, Sweden # # 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 import sys import urllib2 import webbrowser import base64 import datetime import time import os.path import urllib import cookielib from Nagstamon import Actions from Nagstamon.Objects import * from Nagstamon.Server.Generic import GenericServer, not_empty # to let Linux distributions use their own BeautifulSoup if existent try importing local BeautifulSoup first # see https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3302612&group_id=236865 try: from BeautifulSoup import BeautifulSoup except: from Nagstamon.thirdparty.BeautifulSoup import BeautifulSoup class NinjaServer(GenericServer): """ Ninja plugin for Nagstamon """ TYPE = "Ninja" bitmasks = { 1: 'acknowledged', 2: 'notifications_disabled', 4: 'passiveonly', 8: 'scheduled_downtime', 16: 'down_or_unreachable', 32: 'flapping' } commit_path = '/index.php/command/commit' show_login_path = '/index.php/default/show_login' login_path ='/index.php/default/do_login' time_path = '/index.php/extinfo/show_process_info' services_path = "/index.php/status/service/all?servicestatustypes=78&hoststatustypes=71&items_per_page=10000" hosts_path = "/index.php/status/host/?host=all&hoststatustypes=6&items_per_page=999999" # A Monitor CGI URL is not necessary so hide it in settings # autologin is used only by Centreon DISABLED_CONTROLS = ["label_monitor_cgi_url", "input_entry_monitor_cgi_url", "input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # dictionary to translate status bitmaps on webinterface into status flags # this are defaults from Nagios # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Downtime"] def init_config(self): """ dummy init_config, called at thread start, not really needed here, just omit extra properties """ pass def get_start_end(self, host): #try to get ninja3 style update field first last_update = self.FetchURL(self.time_url).result.find("a", {"id": "page_last_updated"}) if not last_update: #maybe ninja2? last_update = self.FetchURL(self.time_url).result.find("span", {"id": "page_last_updated"}) if not last_update: #I don't even ... raise Exception("Failed to get page update time!") start_time = last_update.contents[0] magic_tuple = datetime.datetime.strptime(str(start_time), "%Y-%m-%d %H:%M:%S") start_diff = datetime.timedelta(0, 10) end_diff = datetime.timedelta(0, 7210) start_time = magic_tuple + start_diff end_time = magic_tuple + end_diff return str(start_time), str(end_time) def init_HTTP(self): # add default auth for monitor.old GenericServer.init_HTTP(self) # self.Cookie is a CookieJar which is a list of cookies - if 0 then emtpy if len(self.Cookie) == 0: try: # Ninja Settings # get a Ninja cookie via own method self.urlopener.add_handler(urllib2.HTTPDefaultErrorHandler()) self.urlopener.open(self.login_url, urllib.urlencode({'username': self.get_username(), 'password': self.get_password(), 'csrf_token': self.csrf()})) if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Cookie:" + str(self.Cookie)) except: self.Error(sys.exc_info()) def csrf(self): opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.Cookie)) response = opener.open(self.show_login_url) soup = BeautifulSoup(response.read()) return soup.find('input', {'name': 'csrf_token'})['value'] def open_tree_view(self, host, service): if not service: webbrowser.open('%s/index.php/extinfo/details/host/%s' % (self.monitor_url, host)) else: webbrowser.open('%s/index.php/extinfo/details/service/%s?service=%s' % (self.monitor_url, host, service)) def open_services(self): webbrowser.open('%s/index.php/status/service/all?servicestatustypes=14' % (self.monitor_url)) def open_hosts(self): webbrowser.open('%s/index.php/status/host/all/6' % (self.monitor_url)) @property def time_url(self): return self.monitor_url + self.time_path @property def login_url(self): return self.monitor_url + self.login_path @property def show_login_url(self): return self.monitor_url + self.show_login_path @property def commit_url(self): return self.monitor_url + self.commit_path @property def hosts_url(self): return self.monitor_url + self.hosts_path @property def services_url(self): return self.monitor_url + self.services_path def _set_recheck(self, host, service): if not service: values = {"requested_command": "SCHEDULE_HOST_CHECK"} values.update({"cmd_param[host_name]": host}) else: if self.hosts[host].services[service].is_passive_only(): return values = {"requested_command": "SCHEDULE_SVC_CHECK"} values.update({"cmd_param[service]": host + ";" + service}) content = self.FetchURL(self.time_url, giveback="raw").result pos = content.find('') remote_time = content[pos+len(''):content.find('<', pos+1)] if remote_time: magic_tuple = datetime.datetime.strptime(str(remote_time), "%Y-%m-%d %H:%M:%S") time_diff = datetime.timedelta(0, 10) remote_time = magic_tuple + time_diff if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Get Remote time: " + str(remote_time)) values.update({"cmd_param[check_time]": remote_time}) values.update({"cmd_param[_force]": "1"}) self.FetchURL(self.commit_url, cgi_data=urllib.urlencode(values), giveback="raw") def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services): if not service: values = {"requested_command": "ACKNOWLEDGE_HOST_PROBLEM"} values.update({"cmd_param[host_name]": host}) else: values = {"requested_command": "ACKNOWLEDGE_SVC_PROBLEM"} values.update({"cmd_param[service]": host + ";" + service}) values.update({"cmd_param[sticky]": int(sticky)}) values.update({"cmd_param[notify]": int(notify)}) values.update({"cmd_param[persistent]": int(persistent)}) values.update({"cmd_param[author]": self.get_username()}) values.update({"cmd_param[comment]": comment}) self.FetchURL(self.commit_url, cgi_data=urllib.urlencode(values), giveback="raw") def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): if not service: values = {"requested_command": "SCHEDULE_HOST_DOWNTIME"} values.update({"cmd_param[host_name]": host}) else: values = {"requested_command": "SCHEDULE_SVC_DOWNTIME"} values.update({"cmd_param[service]": host + ";" + service}) values.update({"cmd_param[author]": author}) values.update({"cmd_param[comment]": comment}) values.update({"cmd_param[fixed]": fixed}) values.update({"cmd_param[trigger_id]": "0"}) values.update({"cmd_param[start_time]": start_time}) values.update({"cmd_param[end_time]": end_time}) values.update({"cmd_param[duration]": str(hours) + "." + str(minutes)}) self.FetchURL(self.commit_url, cgi_data=urllib.urlencode(values), giveback="raw") def get_host_status(self): htobj = self.FetchURL(self.hosts_url).result table = htobj.find('table', {'id': 'host_table'}) trs = table.findAll('tr') trs.pop(0) for tr in [tr for tr in table('tr') if len(tr('td')) > 1]: n = self.parse_host_row(tr) # host objects contain service objects if n["name"] not in self.new_hosts: new_host = GenericHost() for attr, val in n.iteritems(): setattr(new_host, attr, val) self.new_hosts[new_host.name] = new_host self.new_hosts[new_host.name].server = self.name del trs, table, htobj def get_service_status(self): htobj = self.FetchURL(self.services_url).result table = htobj.find('table', {'id': 'service_table'}) trs = table('tr') trs.pop(0) lasthost = "" for tr in [tr for tr in table('tr') if len(tr('td')) > 1]: n, host_bitmask = self.parse_service_row(tr) if n["host"] not in self.new_hosts: # the hosts that we just fetched were on a list only containing # those in a non-OK state, thus, we just found a not-yet seen host # and we have to fake it 'til we make it new_host = GenericHost() new_host.name = n["host"] new_host.server = self.name new_host.status = "UP" new_host.visible = False # trying to fix https://sourceforge.net/tracker/index.php?func=detail&aid=3299790&group_id=236865&atid=1101370 # if host is not down but in downtime or any other flag this should be evaluated too if host_bitmask: for number, name in self.bitmasks.iteritems(): setattr(new_host, name, bool(int(host_bitmask) & number)) self.new_hosts[n["host"]] = new_host # if a service does not exist create its object if n["name"] not in self.new_hosts[n["host"]].services: new_service = GenericService() for attr, val in n.iteritems(): setattr(new_service, attr, val) self.new_hosts[n["host"]].services[n["name"]] = new_service self.new_hosts[n["host"]].services[n["name"]].server = self.name del trs, table, htobj def _get_status(self): """ Get status from Ninja Server """ try: self.get_host_status() self.get_service_status() except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) return Result() def parse_host_row(self, tr): tds = tr('td') n = {} n["name"] = tds[0]['id'].split('|')[-1] n["status"] = tds[0]['title'] n["last_check"] = str(tds[5].contents[0]) n["duration"] = str(tds[6].contents[0]) n["attempt"] = "N/A" n["status_information"] = str(tds[7].contents[0]).strip().replace("\n", " ").strip() n["visible"] = True # the last, hidden, span always contains an integer bitmask = tds[2].findAll('span')[-1] for number, name in self.bitmasks.iteritems(): n[name] = bool(int(bitmask.contents[0]) & number) return n def parse_service_row(self, tr): tds = tr('td') n = {} n["status"] = tds[2]['title'] host_bitmask = tds[1].findAll('span') if host_bitmask: # we got at least one hit, pick the last host_bitmask = host_bitmask[-1].contents[0] n["host"] = tds[0]['id'].split('|')[-1] n["name"] = tds[2]['id'].split('|')[-1] n["last_check"] = str(tds[6].contents[0]) n["duration"] = str(tds[7].contents[0]) n["attempt"] = str(tds[8].contents[0]) n["status_information"] = str(tds[9].contents[0]).strip().replace("\n", " ").strip() n["visible"] = True # the last, hidden, span always contains an integer bitmask = tds[4].findAll('span')[-1] for number, name in self.bitmasks.iteritems(): n[name] = bool(int(bitmask.contents[0]) & number) return n, host_bitmask Nagstamon/Nagstamon/Server/Opsview.py000066400000000000000000000266451240775040100202250ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import sys import urllib import webbrowser import copy from Nagstamon import Actions from Nagstamon.Objects import * from Nagstamon.Server.Generic import GenericServer class OpsviewService(GenericService): """ add Opsview specific service property to generic service class """ service_object_id = "" class OpsviewServer(GenericServer): """ special treatment for Opsview XML based API """ TYPE = 'Opsview' # Arguments available for submitting check results SUBMIT_CHECK_RESULT_ARGS = ["comment"] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS= { "monitor": "$MONITOR$/status/service?filter=unhandled&includeunhandledhosts=1",\ "hosts": "$MONITOR$/status/host?hostgroupid=1&state=1",\ "services": "$MONITOR$/status/service?state=1&state=2&state=3",\ "history": "$MONITOR$/event"} # autologin is used only by Centreon DISABLED_CONTROLS = ["input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] def init_HTTP(self): if self.HTTPheaders == {}: GenericServer.init_HTTP(self) # special Opsview treatment, transmit username and passwort for XML requests # http://docs.opsview.org/doku.php?id=opsview3.4:api # this is only necessary when accessing the API and expecting a XML answer self.HTTPheaders["xml"] = {"Content-Type":"text/xml", "X-Username":self.get_username(), "X-Password":self.get_password()} # get cookie to access Opsview web interface to access Opsviews Nagios part if len(self.Cookie) == 0: # put all necessary data into url string logindata = urllib.urlencode({"login_username":self.get_username(),\ "login_password":self.get_password(),\ "back":"",\ "app": "",\ "login":"Log In"}) # the following is necessary for Opsview servers # get cookie from login page via url retrieving as with other urls try: # login and get cookie urlcontent = self.urlopener.open(self.monitor_url + "/login", logindata) urlcontent.close() except: self.Error(sys.exc_info()) def init_config(self): """ dummy init_config, called at thread start, not really needed here, just omit extra properties """ pass def get_start_end(self, host): """ for GUI to get actual downtime start and end from server - they may vary so it's better to get directly from web interface """ try: result = self.FetchURL(self.monitor_cgi_url + "/cmd.cgi?" + urllib.urlencode({"cmd_typ":"55", "host":host})) html = result.result start_time = dict(result.result.find(attrs={"name":"starttime"}).attrs)["value"] end_time = dict(result.result.find(attrs={"name":"endtime"}).attrs)["value"] # give values back as tuple return start_time, end_time except: self.Error(sys.exc_info()) return "n/a", "n/a" def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): # get action url for opsview downtime form if service == "": # host cgi_data = urllib.urlencode({"cmd_typ":"55", "host":host}) else: # service cgi_data = urllib.urlencode({"cmd_typ":"56", "host":host, "service":service}) url = self.monitor_cgi_url + "/cmd.cgi" result = self.FetchURL(url, giveback="raw", cgi_data=cgi_data) html = result.result # which opsview form action to call action = html.split('" enctype="multipart/form-data">')[0].split('action="')[-1] # this time cgi_data does not get encoded because it will be submitted via multipart # to build value for hidden form field old cgi_data is used cgi_data = { "from" : url + "?" + cgi_data, "comment": comment, "starttime": start_time, "endtime": end_time } self.FetchURL(self.monitor_url + action, giveback="raw", cgi_data=cgi_data) def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): """ worker for submitting check result """ # decision about host or service - they have different URLs if service == "": # host - here Opsview uses the plain oldschool Nagios way of CGI url = self.monitor_cgi_url + "/cmd.cgi" cgi_data = urllib.urlencode({"cmd_typ":"87", "cmd_mod":"2", "host":host,\ "plugin_state":{"up":"0", "down":"1", "unreachable":"2"}[state], "plugin_output":check_output,\ "performance_data":performance_data, "btnSubmit":"Commit"}) self.FetchURL(url, giveback="raw", cgi_data=cgi_data) if service != "": # service @ host - here Opsview brews something own url = self.monitor_url + "/state/service/" + self.hosts[host].services[service].service_object_id + "/change" cgi_data = urllib.urlencode({"state":{"ok":"0", "warning":"1", "critical":"2", "unknown":"3"}[state],\ "comment":comment, "submit":"Commit"}) # running remote cgi command self.FetchURL(url, giveback="raw", cgi_data=cgi_data) def _get_status(self): """ Get status from Opsview Server """ # following http://docs.opsview.org/doku.php?id=opsview3.4:api to get ALL services in ALL states except OK # because we filter them out later # the API seems not to let hosts information directly, we hope to get it from service informations try: result = self.FetchURL(self.monitor_url + "/api/status/service?state=1&state=2&state=3", giveback="xml") xmlobj, error = result.result, result.error if error != "": return Result(result=xmlobj, error=copy.deepcopy(error)) for host in xmlobj.data.findAll("list"): # host hostdict = dict(host._getAttrMap()) self.new_hosts[str(hostdict["name"])] = GenericHost() self.new_hosts[str(hostdict["name"])].name = str(hostdict["name"]) self.new_hosts[str(hostdict["name"])].server = self.name # states come in lower case from Opsview self.new_hosts[str(hostdict["name"])].status = str(hostdict["state"].upper()) self.new_hosts[str(hostdict["name"])].status_type = str(hostdict["state_type"]) self.new_hosts[str(hostdict["name"])].last_check = str(hostdict["last_check"]) self.new_hosts[str(hostdict["name"])].duration = Actions.HumanReadableDurationFromSeconds(hostdict["state_duration"]) self.new_hosts[str(hostdict["name"])].attempt = str(hostdict["current_check_attempt"])+ "/" + str(hostdict["max_check_attempts"]) self.new_hosts[str(hostdict["name"])].status_information = str(hostdict["output"].replace("\n", " ")) # if host is in downtime add it to known maintained hosts if hostdict["downtime"] == "2": self.new_hosts[str(hostdict["name"])].scheduled_downtime = True if hostdict.has_key("acknowledged"): self.new_hosts[str(hostdict["name"])].acknowledged = True if hostdict.has_key("flapping"): self.new_hosts[str(hostdict["name"])].flapping = True #services for service in host.findAll("services"): servicedict = dict(service._getAttrMap()) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])] = OpsviewService() self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].host = str(hostdict["name"]) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].name = str(servicedict["name"]) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].server = self.name # states come in lower case from Opsview self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].status = str(servicedict["state"].upper()) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].status_type = str(servicedict["state_type"]) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].last_check = str(servicedict["last_check"]) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].duration = Actions.HumanReadableDurationFromSeconds(servicedict["state_duration"]) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].attempt = str(servicedict["current_check_attempt"])+ "/" + str(servicedict["max_check_attempts"]) self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].status_information= str(servicedict["output"].replace("\n", " ")) if servicedict["downtime"] == "2": self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].scheduled_downtime = True if servicedict.has_key("acknowledged"): self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].acknowledged = True if servicedict.has_key("flapping"): self.new_hosts[str(hostdict["name"])].services[str(servicedict["name"])].flapping = True # extra opsview id for service, needed for submitting check results self.new_hosts[str(str(hostdict["name"]))].services[str(str(servicedict["name"]))].service_object_id = str(servicedict["service_object_id"]) del servicedict del hostdict except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) #dummy return in case all is OK return Result() def open_tree_view(self, host, service): webbrowser.open('%s/status/service?host=%s' % (self.monitor_url, host)) Nagstamon/Nagstamon/Server/Thruk.py000066400000000000000000000342341240775040100176570ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # Thruk additions copyright by dcec@Github # # 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 from Nagstamon.Server.Generic import GenericServer import sys import cookielib import base64 import json import datetime import urllib import copy # to let Linux distributions use their own BeautifulSoup if existent try importing local BeautifulSoup first # see https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3302612&group_id=236865 try: from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup except: from Nagstamon.thirdparty.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup from Nagstamon.Actions import HostIsFilteredOutByRE, ServiceIsFilteredOutByRE, StatusInformationIsFilteredOutByRE, not_empty from Nagstamon.Objects import * class ThrukServer(GenericServer): """ Thruk is derived from generic (Nagios) server """ TYPE = 'Thruk' # GUI sortable columns stuff DEFAULT_SORT_COLUMN_ID = 2 # lost any memory what this COLOR_COLUMN_ID is used for... #COLOR_COLUMN_ID = 2 HOST_COLUMN_ID = 0 SERVICE_COLUMN_ID = 1 # used for $STATUS$ variable for custom actions STATUS_INFO_COLUMN_ID = 6 COLUMNS = [ HostColumn, ServiceColumn, StatusColumn, LastCheckColumn, DurationColumn, AttemptColumn, StatusInformationColumn ] # autologin is used only by Centreon DISABLED_CONTROLS = ["input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] # dictionary to translate status bitmaps on webinterface into status flags # this are defaults from Nagios # "disabled.gif" is in Nagios for hosts the same as "passiveonly.gif" for services STATUS_MAPPING = { "ack.gif" : "acknowledged",\ "passiveonly.gif" : "passiveonly",\ "disabled.gif" : "passiveonly",\ "ndisabled.gif" : "notifications_disabled",\ "downtime.gif" : "scheduled_downtime",\ "flapping.gif" : "flapping"} # Entries for monitor default actions in context menu MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Submit check result", "Downtime"] # Arguments available for submitting check results SUBMIT_CHECK_RESULT_ARGS = ["check_output", "performance_data"] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS = { "monitor": "$MONITOR$",\ "hosts": "$MONITOR-CGI$/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&page=1&entries=all",\ "services": "$MONITOR-CGI$/status.cgi?dfl_s0_value_sel=5&dfl_s0_servicestatustypes=29&dfl_s0_op=%3D&style=detail&dfl_s0_type=host&dfl_s0_serviceprops=0&dfl_s0_servicestatustype=4&dfl_s0_servicestatustype=8&dfl_s0_servicestatustype=16&dfl_s0_servicestatustype=1&hidetop=&dfl_s0_hoststatustypes=15&dfl_s0_val_pre=&hidesearch=2&dfl_s0_value=all&dfl_s0_hostprops=0&nav=&page=1&entries=all",\ "history": "$MONITOR-CGI$/history.cgi?host=all&page=1&entries=all"} STATES_MAPPING = {"hosts" : {0 : "OK", 1 : "DOWN", 2 : "UNREACHABLE"},\ "services" : {0 : "OK", 1 : "WARNING", 2 : "CRITICAL", 3 : "UNKNOWN"}} def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # flag for newer cookie authentication self.CookieAuth = False def init_HTTP(self): """ partly not constantly working Basic Authorization requires extra Autorization headers, different between various server types """ GenericServer.init_HTTP(self) #if self.HTTPheaders == {}: # for giveback in ["raw", "obj"]: # self.HTTPheaders[giveback] = {"Authorization": "Basic " + base64.b64encode(self.get_username() + ":" + self.get_password())} # only if cookies are needed if self.CookieAuth: # get cookie to access Check_MK web interface if len(self.Cookie) < 2: # put all necessary data into url string logindata = urllib.urlencode({"login":self.get_username(),\ "password":self.get_password(),\ "submit":"Login"}) # get cookie from login page via url retrieving as with other urls try: # login and get cookie # empty referer seems to be ignored so add it manually urlcontent = self.urlopener.open(self.monitor_cgi_url + "/login.cgi?", logindata + "&referer=") urlcontent.close() except: self.Error(sys.exc_info()) def init_config(self): """ set URLs for CGI - they are static and there is no need to set them with every cycle """ # create filters like described in # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes # Thruk allows requesting only needed information to reduce traffic self.cgiurl_services = self.monitor_cgi_url + "/status.cgi?host=all&servicestatustypes=28&view_mode=json&"\ "entries=all&columns=host_name,description,state,last_check,"\ "last_state_change,plugin_output,current_attempt,"\ "max_check_attempts,active_checks_enabled,is_flapping,"\ "notifications_enabled,acknowledged,state_type,"\ "scheduled_downtime_depth" # hosts (up or down or unreachable) self.cgiurl_hosts = self.monitor_cgi_url + "/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&"\ "view_mode=json&entries=all&"\ "columns=name,state,last_check,last_state_change,"\ "plugin_output,current_attempt,max_check_attempts,"\ "active_checks_enabled,notifications_enabled,is_flapping,"\ "acknowledged,scheduled_downtime_depth,state_type" # test for cookies # put all necessary data into url string logindata = urllib.urlencode({"login":self.get_username(),\ "password":self.get_password(),\ "submit":"Login"}) # get cookie from login page via url retrieving as with other urls try: # login and get cookie # empty referer seems to be ignored so add it manually urlcontent = self.urlopener.open(self.monitor_cgi_url + "/login.cgi?", logindata + "&referer=") urlcontent.close() if len(self.Cookie) > 0: self.CookieAuth = True except: self.Error(sys.exc_info()) def _get_status(self): """ Get status from Thruk Server """ # new_hosts dictionary self.new_hosts = dict() # hosts - mostly the down ones # unfortunately the hosts status page has a different structure so # hosts must be analyzed separately try: # JSON experiments result = self.FetchURL(self.cgiurl_hosts, giveback="raw") jsonraw, error = copy.deepcopy(result.result), copy.deepcopy(result.error) if error != "": return Result(result=jsonraw, error=error) # in case basic auth did not work try form login cookie based login if jsonraw.startswith("<"): self.CookieAuth = True return Result(result=None, error="Login failed.") # in case JSON is not empty evaluate it elif not jsonraw == "[]": hosts = json.loads(jsonraw) for h in hosts: if not self.new_hosts.has_key(h["name"]): ###new_host = h["name"] self.new_hosts[h["name"]] = GenericHost() self.new_hosts[h["name"]].name = h["name"] self.new_hosts[h["name"]].server = self.name self.new_hosts[h["name"]].status = self.STATES_MAPPING["hosts"][h["state"]] self.new_hosts[h["name"]].last_check = datetime.datetime.fromtimestamp(int(h["last_check"])).isoformat(" ") self.new_hosts[h["name"]].duration = Actions.HumanReadableDurationFromTimestamp(h["last_state_change"]) self.new_hosts[h["name"]].attempt = "%s/%s" % (h["current_attempt"], h["max_check_attempts"]) self.new_hosts[h["name"]].status_information= h["plugin_output"].encode("utf-8").replace("\n", " ").strip() self.new_hosts[h["name"]].passiveonly = not(bool(int(h["active_checks_enabled"]))) self.new_hosts[h["name"]].notifications_disabled = bool(int(h["is_flapping"])) self.new_hosts[h["name"]].flapping = bool(int(h["is_flapping"])) self.new_hosts[h["name"]].acknowledged = bool(int(h["acknowledged"])) self.new_hosts[h["name"]].scheduled_downtime = bool(int(h["scheduled_downtime_depth"])) self.new_hosts[h["name"]].status_type = {0: "soft", 1: "hard"}[h["state_type"]] del h except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # services try: # JSON experiments result = self.FetchURL(self.cgiurl_services, giveback="raw") jsonraw, error = copy.deepcopy(result.result), copy.deepcopy(result.error) if error != "": return Result(result=jsonraw, error=error) # in case basic auth did not work try form login cookie based login if jsonraw.startswith("<"): self.CookieAuth = True return Result(result=None, error="Login failed.") # in case JSON is not empty evaluate it elif not jsonraw == "[]": services = json.loads(jsonraw) for s in services: # host objects contain service objects if not self.new_hosts.has_key(s["host_name"]): self.new_hosts[s["host_name"]] = GenericHost() self.new_hosts[s["host_name"]].name = s["host_name"] self.new_hosts[s["host_name"]].server = self.name self.new_hosts[s["host_name"]].status = "UP" # if a service does not exist create its object if not self.new_hosts[s["host_name"]].services.has_key(s["description"]): ###new_service = s["description"] self.new_hosts[s["host_name"]].services[s["description"]] = GenericService() self.new_hosts[s["host_name"]].services[s["description"]].host = s["host_name"] self.new_hosts[s["host_name"]].services[s["description"]].name = s["description"] self.new_hosts[s["host_name"]].services[s["description"]].server = self.name self.new_hosts[s["host_name"]].services[s["description"]].status = self.STATES_MAPPING["services"][s["state"]] self.new_hosts[s["host_name"]].services[s["description"]].last_check = datetime.datetime.fromtimestamp(int(s["last_check"])).isoformat(" ") self.new_hosts[s["host_name"]].services[s["description"]].duration = Actions.HumanReadableDurationFromTimestamp(s["last_state_change"]) self.new_hosts[s["host_name"]].services[s["description"]].attempt = "%s/%s" % (s["current_attempt"], s["max_check_attempts"]) self.new_hosts[s["host_name"]].services[s["description"]].status_information = s["plugin_output"].encode("utf-8").replace("\n", " ").strip() self.new_hosts[s["host_name"]].services[s["description"]].passiveonly = not(bool(int(s["active_checks_enabled"]))) self.new_hosts[s["host_name"]].services[s["description"]].notifications_disabled = not(bool(int(s["notifications_enabled"]))) self.new_hosts[s["host_name"]].services[s["description"]].flapping = bool(int(s["is_flapping"])) self.new_hosts[s["host_name"]].services[s["description"]].acknowledged = bool(int(s["acknowledged"])) self.new_hosts[s["host_name"]].services[s["description"]].scheduled_downtime = bool(int(s["scheduled_downtime_depth"])) self.new_hosts[s["host_name"]].services[s["description"]].status_type = {0: "soft", 1: "hard"}[s["state_type"]] del s except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) #dummy return in case all is OK return Result() Nagstamon/Nagstamon/Server/Zabbix.py000066400000000000000000000426711240775040100200050ustar00rootroot00000000000000#!/usr/bin/python # -*- encoding: utf-8; py-indent-offset: 4 -*- # # Zabbix.py based on Check_MK Multisite.py # # +------------------------------------------------------------------+ # | ____ _ _ __ __ _ __ | # | / ___| |__ ___ ___| | __ | \/ | |/ / | # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / | # | | |___| | | | __/ (__| < | | | | . \ | # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ | # | | # | Copyright Mathias Kettner 2010 mk@mathias-kettner.de | # | lm@mathias-kettner.de | # +------------------------------------------------------------------+ # # The official homepage is at http://mathias-kettner.de/check_mk. # # check_mk 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 in version 2. check_mk is distributed # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with- # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. See the GNU General Public License for more de- # ails. You should have received a copy of the GNU General Public # License along with GNU Make; see the file COPYING. If not, write # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301 USA. # hax0rized by: lm@mathias-kettner.de import sys import urllib import webbrowser import base64 import time import datetime from Nagstamon import Actions from Nagstamon.Objects import * from Nagstamon.Server.Generic import GenericServer from Nagstamon.thirdparty.zabbix_api import ZabbixAPI, ZabbixAPIException class ZabbixError(Exception): def __init__(self, terminate, result): self.terminate = terminate self.result = result class ZabbixServer(GenericServer): """ special treatment for Zabbix, taken from Check_MK Multisite JSON API """ TYPE = 'Zabbix' zapi = None # A Monitor CGI URL is not necessary so hide it in settings # autologin is used only by Centreon DISABLED_CONTROLS = ["label_monitor_cgi_url", "input_entry_monitor_cgi_url", "input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Prepare all urls needed by nagstamon - self.urls = {} self.statemap = {} # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Recheck", "Acknowledge", "Downtime"] self.username = self.conf.servers[self.get_name()].username self.password = self.conf.servers[self.get_name()].password def _login(self): try: self.zapi = ZabbixAPI(server=self.monitor_url, path="", log_level=0) self.zapi.login(self.username, self.password) except ZabbixAPIException: result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) def init_HTTP(self): self.statemap = { 'UNREACH': 'UNREACHABLE', 'CRIT': 'CRITICAL', 'WARN': 'WARNING', 'UNKN': 'UNKNOWN', 'PEND': 'PENDING', '0': 'OK', '1': 'UNKNOWN', '2': 'WARNING', '5': 'CRITICAL', '3': 'WARNING', '4': 'CRITICAL'} GenericServer.init_HTTP(self) def _get_status(self): """ Get status from Nagios Server """ ret = Result() # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily nagitems = {"services": [], "hosts": []} # Create URLs for the configured filters if self.zapi is None: self._login() try: hosts = [] try: hosts = self.zapi.host.get( {"output": ["host", "ip", "status", "available", "error", "errors_from"], "filter": {}}) except: # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) for host in hosts: n = { 'host': host['host'], 'status': self.statemap.get(host['available'], host['available']), 'last_check': 'n/a', 'duration': Actions.HumanReadableDurationFromTimestamp(host['errors_from']), 'status_information': host['error'], 'attempt': '1/1', 'site': '', 'address': host['host'], } # add dictionary full of information about this host item to nagitems nagitems["hosts"].append(n) # after collection data in nagitems create objects from its informations # host objects contain service objects if n["host"] not in self.new_hosts: new_host = n["host"] self.new_hosts[new_host] = GenericHost() self.new_hosts[new_host].name = n["host"] self.new_hosts[new_host].status = n["status"] self.new_hosts[new_host].last_check = n["last_check"] self.new_hosts[new_host].duration = n["duration"] self.new_hosts[new_host].attempt = n["attempt"] self.new_hosts[new_host].status_information = n["status_information"] self.new_hosts[new_host].site = n["site"] self.new_hosts[new_host].address = n["address"] except ZabbixError: self.isChecking = False result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) # services services = [] groupids = [] zabbix_triggers = [] try: api_version = self.zapi.api_version() except ZabbixAPIException: # FIXME Is there a cleaner way to handle this? I just borrowed # this code from 80 lines ahead. -- AGV # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) print sys.exc_info() return Result(result=result, error=error) try: response = [] try: #service = self.zapi.trigger.get({"select_items":"extend","monitored":1,"only_true":1,"min_severity":3,"output":"extend","filter":{}}) triggers_list = [] if self.monitor_cgi_url: group_list = self.monitor_cgi_url.split(',') #hostgroup_ids = [x['groupid'] for x in self.zapi.hostgroup.get( # {'output': 'extend', # 'with_monitored_items': True, # 'filter': {"name": group_list}}) if int(x['internal']) == 0] # only without filter there is anything shown at all hostgroup_ids = [x['groupid'] for x in self.zapi.hostgroup.get( {'output': 'extend', 'with_monitored_items': True}) if int(x['internal']) == 0] zabbix_triggers = self.zapi.trigger.get( {'sortfield': 'lastchange', 'withUnacknowledgedEvents': True, 'groupids': hostgroup_ids, "monitored": True, "filter": {'value': 1}}) else: zabbix_triggers = self.zapi.trigger.get( {'sortfield': 'lastchange', 'withUnacknowledgedEvents': True, "monitored": True, "filter": {'value': 1}}) triggers_list = [] for trigger in zabbix_triggers: triggers_list.append(trigger.get('triggerid')) this_trigger = self.zapi.trigger.get( {'triggerids': triggers_list, 'expandDescription': True, 'output': 'extend', 'select_items': 'extend', 'expandData': True} ) if type(this_trigger) is dict: for triggerid in this_trigger.keys(): services.append(this_trigger[triggerid]) elif type(this_trigger) is list: for trigger in this_trigger: services.append(trigger) except ZabbixAPIException: # FIXME Is there a cleaner way to handle this? I just borrowed # this code from 80 lines ahead. -- AGV # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) print sys.exc_info() return Result(result=result, error=error) except ZabbixError, e: #print "------------------------------------" #print "%s" % e.result.error if e.terminate: return e.result else: service = e.result.content ret = e.result for service in services: if api_version > '1.8': state = '%s' % service['description'] else: state = '%s=%s' % (service['items'][0]['key_'], service['items'][0]['lastvalue']) n = { 'host': service['host'], 'service': service['description'], 'status': self.statemap.get(service['priority'], service['priority']), # 1/1 attempt looks at least like there has been any attempt 'attempt': '1/1', 'duration': Actions.HumanReadableDurationFromTimestamp(service['lastchange']), 'status_information': state, 'passiveonly': 'no', 'last_check': 'n/a', 'notifications': 'yes', 'flapping': 'no', 'site': '', 'command': 'zabbix', 'triggerid': service['triggerid'], } nagitems["services"].append(n) # after collection data in nagitems create objects of its informations # host objects contain service objects if n["host"] not in self.new_hosts: self.new_hosts[n["host"]] = GenericHost() self.new_hosts[n["host"]].name = n["host"] self.new_hosts[n["host"]].status = "UP" self.new_hosts[n["host"]].site = n["site"] self.new_hosts[n["host"]].address = n["host"] # if a service does not exist create its object if n["service"] not in self.new_hosts[n["host"]].services: # workaround for non-existing (or not found) host status flag if n["service"] == "Host is down %s" % (n["host"]): self.new_hosts[n["host"]].status = "DOWN" # also take duration from "service" aka trigger self.new_hosts[n["host"]].duration = n["duration"] else: new_service = n["service"] self.new_hosts[n["host"]].services[new_service] = GenericService() self.new_hosts[n["host"]].services[new_service].host = n["host"] # next dirty workaround to get Zabbix events to look Nagios-esque if (" on " or " is ") in n["service"]: for separator in [" on ", " is "]: n["service"] = n["service"].split(separator)[0] self.new_hosts[n["host"]].services[new_service].name = n["service"] self.new_hosts[n["host"]].services[new_service].status = n["status"] self.new_hosts[n["host"]].services[new_service].last_check = n["last_check"] self.new_hosts[n["host"]].services[new_service].duration = n["duration"] self.new_hosts[n["host"]].services[new_service].attempt = n["attempt"] self.new_hosts[n["host"]].services[new_service].status_information = n["status_information"] #self.new_hosts[n["host"]].services[new_service].passiveonly = n["passiveonly"] self.new_hosts[n["host"]].services[new_service].passiveonly = False #self.new_hosts[n["host"]].services[new_service].flapping = n["flapping"] self.new_hosts[n["host"]].services[new_service].flapping = False self.new_hosts[n["host"]].services[new_service].site = n["site"] self.new_hosts[n["host"]].services[new_service].address = n["host"] self.new_hosts[n["host"]].services[new_service].command = n["command"] self.new_hosts[n["host"]].services[new_service].triggerid = n["triggerid"] except (ZabbixError, ZabbixAPIException): # set checking flag back to False self.isChecking = False result, error = self.Error(sys.exc_info()) print sys.exc_info() return Result(result=result, error=error) return ret def _open_browser(self, url): webbrowser.open(url) if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), debug="Open web page " + url) def open_services(self): self._open_browser(self.urls['human_services']) def open_hosts(self): self._open_browser(self.urls['human_hosts']) def open_tree_view(self, host, service=""): """ open monitor from treeview context menu """ if service == "": url = self.urls['human_host'] + urllib.urlencode( {'x': 'site=' + self.hosts[host].site + '&host=' + host}).replace('x=', '%26') else: url = self.urls['human_service'] + urllib.urlencode( {'x': 'site=' + self.hosts[host].site + '&host=' + host + '&service=' + service}).replace('x=', '%26') if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, service=service, debug="Open host/service monitor web page " + url) webbrowser.open(url) def GetHost(self, host): """ find out ip or hostname of given host to access hosts/devices which do not appear in DNS but have their ip saved in Nagios """ # the fasted method is taking hostname as used in monitor if str(self.conf.connect_by_host) == "True": return Result(result=host) ip = "" try: if host in self.hosts: ip = self.hosts[host].address if str(self.conf.debug_mode) == "True": self.Debug(server=self.get_name(), host=host, debug="IP of %s:" % host + " " + ip) if str(self.conf.connect_by_dns) == "True": try: address = socket.gethostbyaddr(ip)[0] except: address = ip else: address = ip except ZabbixError: result, error = self.Error(sys.exc_info()) return Result(result=result, error=error) return Result(result=address) def _set_recheck(self, host, service): pass def get_start_end(self, host): return time.strftime("%Y-%m-%d %H:%M"), time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() + 7200)) def _action(self, site, host, service, specific_params): params = { 'site': self.hosts[host].site, 'host': host, } params.update(specific_params) if self.zapi is None: self._login() events = [] for e in self.zapi.event.get({'triggerids': params['triggerids'], 'hide_unknown': True, 'sortfield': 'clock', 'sortorder': 'desc'}): events.append(e['eventid']) self.zapi.event.acknowledge({'eventids': events, 'message': params['message']}) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): pass def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=[]): triggerid = self.hosts[host].services[service].triggerid p = { 'message': '%s: %s' % (author, comment), 'triggerids': [triggerid], } self._action(self.hosts[host].site, host, service, p) # acknowledge all services on a host when told to do so for s in all_services: self._action(self.hosts[host].site, host, s, p) Nagstamon/Nagstamon/Server/__init__.py000066400000000000000000000015501240775040100203140ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 """Module Nagstamon""" Nagstamon/Nagstamon/Server/op5Monitor.py000066400000000000000000000363211240775040100206340ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 import sys import json import urllib import datetime import time from datetime import datetime from Nagstamon import Actions from Nagstamon.Objects import * from Nagstamon.Server.Generic import GenericServer, not_empty def human_duration(start): """ transform timestamp to human readable some changes necessary due to https://github.com/HenriWahl/Nagstamon/issues/93 - move definition of stop out of def() statement because it kept static """ stop = time.time() if stop <= start: return "n/a" else: ret = '' first = True secs = stop - start units = 'wdhms' divisors = {'w': 86400 * 7, 'd': 86400, 'h': 3600, 'm': 60, 's': 1} for unit in units: divisor = divisors[unit] if secs < divisor: continue amount = int(secs / divisor) secs %= divisor if not first: ret += ' ' ret += "%d%c" % (amount, unit) first = False return ret class Op5MonitorServer(GenericServer): """ object of Nagios server - when nagstamon will be able to poll various servers this will be useful As Nagios is the default server type all its methods are in GenericServer """ TYPE = 'op5Monitor' api_count='/api/filter/count/?query=' api_query='/api/filter/query/?query=' api_cmd='/api/command' api_svc_col = [] api_host_col = [] api_host_col.append('acknowledged') api_host_col.append('active_checks_enabled') api_host_col.append('alias') api_host_col.append('current_attempt') api_host_col.append('is_flapping') api_host_col.append('last_check') api_host_col.append('last_state_change') api_host_col.append('max_check_attempts') api_host_col.append('name') api_host_col.append('notifications_enabled') api_host_col.append('plugin_output') api_host_col.append('scheduled_downtime_depth') api_host_col.append('state') api_svc_col.append('acknowledged') api_svc_col.append('active_checks_enabled') api_svc_col.append('current_attempt') api_svc_col.append('description') api_svc_col.append('host.name') api_svc_col.append('host.state') api_svc_col.append('host.active_checks_enabled') api_svc_col.append('is_flapping') api_svc_col.append('last_check') api_svc_col.append('last_state_change') api_svc_col.append('max_check_attempts') api_svc_col.append('notifications_enabled') api_svc_col.append('plugin_output') api_svc_col.append('scheduled_downtime_depth') api_svc_col.append('state') api_default_svc_query='[services] state !=0' api_default_svc_query+=' or host.state != 0' api_default_svc_query+='&columns=%s' % (','.join(api_svc_col)) api_default_svc_query+='&format=json' api_default_host_query='[hosts] state !=0' api_default_host_query+='&columns=%s' % (','.join(api_host_col)) api_default_host_query+='&format=json' api_default_host_query = api_default_host_query.replace(" ", "%20") api_default_svc_query = api_default_svc_query.replace(" ", "%20") # autologin is used only by Centreon DISABLED_CONTROLS = ["label_monitor_cgi_url", "input_entry_monitor_cgi_url", "input_checkbutton_use_autologin", "label_autologin_key", "input_entry_autologin_key", "input_checkbutton_use_display_name_host", "input_checkbutton_use_display_name_service"] # URLs for browser shortlinks/buttons on popup window BROWSER_URLS = { "monitor": "$MONITOR$/monitor",\ "hosts": "$MONITOR$/monitor/index.php/listview?q=%s" % '[hosts] all and state != 0'.replace(" ", "%20"),\ "services": "$MONITOR$/monitor/index.php/listview?q=%s" % '[services] all and state != 0'.replace(" ", "%20"),\ "history": "$MONITOR$/monitor/index.php/alert_history/generate"} def __init__(self, **kwds): GenericServer.__init__(self, **kwds) # Entries for monitor default actions in context menu self.MENU_ACTIONS = ["Monitor", "Recheck", "Acknowledge", "Downtime"] self.STATUS_SVC_MAPPING = {'0':'OK', '1':'WARNING', '2':'CRITICAL', '3':'UNKNOWN'} self.STATUS_HOST_MAPPING = {'0':'UP', '1':'DOWN', '2':'UNREACHABLE'} def _get_status(self): """ Get status from op5 Monitor Server """ # create Nagios items dictionary with to lists for services and hosts # every list will contain a dictionary for every failed service/host # this dictionary is only temporarily nagitems = {"hosts":[], "services":[]} # new_hosts dictionary self.new_hosts = dict() # Fetch api listview with filters try: # Fetch Host info result = self.FetchURL(self.monitor_url + self.api_count + self.api_default_host_query, giveback="raw") data = json.loads(result.result) if data['count']: count = data['count'] result = self.FetchURL(self.monitor_url + self.api_query + self.api_default_host_query + '&limit=' + str(count), giveback="raw") data = json.loads(result.result) n = dict() for api in data: n['host'] = api['name'] n["acknowledged"] = api['acknowledged'] n["flapping"] = api['is_flapping'] n["notifications_disabled"] = 0 if api['notifications_enabled'] else 1 n["passiveonly"] = 0 if api['active_checks_enabled'] else 1 n["scheduled_downtime"] = 1 if api['scheduled_downtime_depth'] else 0 n['attempt'] = "%s/%s" % (str(api['current_attempt']), str(api['max_check_attempts'])) n['duration'] = human_duration(api['last_state_change']) n['last_check'] = datetime.fromtimestamp(int(api['last_check'])).strftime('%Y-%m-%d %H:%M:%S') n['status'] = self.STATUS_HOST_MAPPING[str(api['state'])] n['status_information'] = api['plugin_output'] n['status_type'] = api['state'] if not self.new_hosts.has_key(n['host']): self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].acknowledged = n["acknowledged"] self.new_hosts[n['host']].attempt = n['attempt'] self.new_hosts[n['host']].duration = n['duration'] self.new_hosts[n['host']].flapping = n["flapping"] self.new_hosts[n['host']].last_check = n['last_check'] self.new_hosts[n['host']].notifications_disabled = n["notifications_disabled"] self.new_hosts[n['host']].passiveonly = n["passiveonly"] self.new_hosts[n['host']].scheduled_downtime = n["scheduled_downtime"] self.new_hosts[n['host']].status = n['status'] self.new_hosts[n['host']].status_information = n['status_information'].replace("\n", " ").strip() self.new_hosts[n['host']].status_type = n['status_type'] nagitems['hosts'].append(n) del n # Fetch services info result = self.FetchURL(self.monitor_url + self.api_count + self.api_default_svc_query, giveback="raw") data = json.loads(result.result) if data['count']: count = data['count'] result = self.FetchURL(self.monitor_url + self.api_query + self.api_default_svc_query + '&limit=' + str(count), giveback="raw") data = json.loads(result.result) for api in data: n = dict() n['host'] = api['host']['name'] n['status'] = self.STATUS_HOST_MAPPING[str(api['host']['state'])] n["passiveonly"] = 0 if api['host']['active_checks_enabled'] else 1 if not self.new_hosts.has_key(n['host']): self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = n['status'] self.new_hosts[n['host']].passiveonly = n["passiveonly"] n['service'] = api['description'] n["acknowledged"] = api['acknowledged'] n["flapping"] = api['is_flapping'] n["notifications_disabled"] = 0 if api['notifications_enabled'] else 1 n["passiveonly"] = 0 if api['active_checks_enabled'] else 1 n["scheduled_downtime"] = 1 if api['scheduled_downtime_depth'] else 0 n['attempt'] = "%s/%s" % (str(api['current_attempt']), str(api['max_check_attempts'])) n['duration'] = human_duration(api['last_state_change']) n['last_check'] = datetime.fromtimestamp(int(api['last_check'])).strftime('%Y-%m-%d %H:%M:%S') n['status_information'] = api['plugin_output'] if not self.new_hosts.has_key(n['host']): self.new_hosts[n['host']] = GenericHost() self.new_hosts[n['host']].name = n['host'] self.new_hosts[n['host']].status = n['status'] if not self.new_hosts[n['host']].services.has_key(n['service']): n['status'] = self.STATUS_SVC_MAPPING[str(api['state'])] self.new_hosts[n['host']].services[n['service']] = GenericService() self.new_hosts[n['host']].services[n['service']].acknowledged = n['acknowledged'] self.new_hosts[n['host']].services[n['service']].attempt = n['attempt'] self.new_hosts[n['host']].services[n['service']].duration = n['duration'] self.new_hosts[n['host']].services[n['service']].flapping = n['flapping'] self.new_hosts[n['host']].services[n['service']].host = n['host'] self.new_hosts[n['host']].services[n['service']].last_check = n['last_check'] self.new_hosts[n['host']].services[n['service']].name = n['service'] self.new_hosts[n['host']].services[n['service']].notifications_disabled = n["notifications_disabled"] self.new_hosts[n['host']].services[n['service']].passiveonly = n['passiveonly'] self.new_hosts[n['host']].services[n['service']].scheduled_downtime = n['duration'] self.new_hosts[n['host']].services[n['service']].scheduled_downtime = n['scheduled_downtime'] self.new_hosts[n['host']].services[n['service']].status = n['status'] self.new_hosts[n['host']].services[n['service']].status_information = n['status_information'].replace("\n", " ").strip() nagitems['services'].append(n) return Result() except: print "========================================== b0rked ==========================================" self.isChecking = False result,error = self.Error(sys.exc_info()) print error return Result(result=result, error=error) return Result() def open_tree_view(self, host, service): if not service: url = "%s/monitor/index.php/extinfo/details?host=%s" % (self.monitor_url, host) else: url = "%s/monitor/index.php/extinfo/details?host=%s&service=%s" % (self.monitor_url, host, service) action = Actions.Action(type="browser", string=url, conf=self.conf, server=self, host=host, service=service) action.run() def get_start_end(self, host): return time.strftime("%Y-%m-%d %H:%M"), time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() + 7200)) def send_command(self, command, params=False): url = self.monitor_url + self.api_cmd + '/' + command if 'service_description' in params.keys(): action = Actions.Action( type="url-post", string=url, cgi_data=urllib.urlencode(params), conf=self.conf, server=self, host=params["host_name"], service=params["service_description"], ) else: action = Actions.Action( type="url-post", string=url, cgi_data=urllib.urlencode(params), conf=self.conf, server=self, host=params["host_name"], ) action.run() def _set_recheck(self, host, service): params = {'host_name': host, 'check_time': int(time.time())} if not service: command = 'SCHEDULE_HOST_CHECK' else: if self.hosts[host].services[service].is_passive_only(): return command = 'SCHEDULE_SVC_CHECK' params['service_description'] = service self.send_command(command, params) def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services): params = {'host_name': host, 'sticky': int(sticky), 'notify': int(notify), 'persistent': int(persistent), 'comment': comment} if not service: command = 'ACKNOWLEDGE_HOST_PROBLEM' else: params['service_description'] = service command = 'ACKNOWLEDGE_SVC_PROBLEM' self.send_command(command, params) def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): start_time = int(time.mktime(time.strptime(start_time, "%Y-%m-%d %H:%M"))) end_time = int(time.mktime(time.strptime(end_time, "%Y-%m-%d %H:%M"))) duration = end_time - start_time params = {'host_name': host, 'comment': comment, 'fixed': fixed, 'trigger_id': '0', 'start_time': start_time, 'end_time': end_time, 'duration': duration} if not service: command = 'SCHEDULE_HOST_DOWNTIME' else: command = 'SCHEDULE_SVC_DOWNTIME' params['service_description'] = service self.send_command(command, params) Nagstamon/Nagstamon/__init__.py000066400000000000000000000015501240775040100170460ustar00rootroot00000000000000# encoding: utf-8 # Nagstamon - Nagios status monitor for your desktop # Copyright (C) 2008-2014 Henri Wahl et al. # # 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 """Module Nagstamon""" Nagstamon/Nagstamon/resources/000077500000000000000000000000001240775040100167465ustar00rootroot00000000000000Nagstamon/Nagstamon/resources/LICENSE000066400000000000000000001323301240775040100177550ustar00rootroot00000000000000Nagstamon is licensed under the following GPL2: 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. Copyright (C) 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. , 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. Nagstamon uses BeautifulSoup under the following license: Copyright (c) 2004-2010, Leonard Richardson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the the Beautiful Soup Consortium and All Night Kosher Bakery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. Nagstamon's experimental Zabbix support is based on zabbix_api.py, which is licensed under LGPL 2.1: GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 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. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, 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 library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete 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 distribute a copy of this License along with the Library. 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 Library or any portion of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, 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 Library, 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 Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you 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. If distribution of 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 satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be 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. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library 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. 9. 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 Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library 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 with this License. 11. 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 Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library 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 Library. 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. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library 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. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library 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 Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, 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 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. 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 LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), 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 Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. 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. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; 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. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it!Nagstamon/Nagstamon/resources/acknowledge_dialog.ui000066400000000000000000000345261240775040100231210ustar00rootroot00000000000000 False Acknowledge False True center-always normal True False True False 10 2 Change acknowledgement defaults... True True False 1 2 9 10 5 5 True True False False True True 1 2 8 9 5 5 True False 0 Comment: 8 9 5 5 True False 0 Author: 7 8 5 5 True True True False False False True True 1 2 7 8 5 5 Acknowledge all services on host True True False 3 0 True 1 2 6 7 5 5 Persistent comment True True False 3 0 True 1 2 5 6 5 5 Send notification True True False 3 0 True 1 2 4 5 5 5 Sticky acknowledgement True True False 3 0 True 1 2 3 4 5 5 True False 0 Options: 3 4 5 5 True False 0 description - set by GUI.py True 2 2 3 5 5 True False hidden service 1 2 True False hidden hostname False True 0 True False end OK True True True True False False 0 Cancel True True False False False 1 False True end 1 button_ok button_cancel Nagstamon/Nagstamon/resources/authentication_dialog.ui000066400000000000000000000325771240775040100236610ustar00rootroot00000000000000 False Nagstamon authentification False True center-always True dialog True False True False end OK True True True True False False True 0 Exit Nagstamon True True True False True 1 Disable monitor True True True False True 2 False True end 0 True False 3 6 2 True False True Username of your Nagios website. Username of your Nagios website. 0 Username: 1 2 5 5 True True 35 user False False True True 1 2 1 2 5 5 True True True True False True 35 password False False True True 1 2 2 3 5 5 True False True Password for your Nagios website. Password for your Nagios website. 0 Password: 2 3 5 5 Save password True True False 0 True 1 2 3 4 5 5 True False 0 3 3 Please give the correct credentials. 2 5 5 Use autologin True True False 0 True 1 2 4 5 5 5 True False 0 Autologin Key: 5 6 5 5 True True False False True True 1 2 5 6 5 5 False True 2 button_ok button_exit button_disable Nagstamon/Nagstamon/resources/close.png000066400000000000000000000017461240775040100205710ustar00rootroot00000000000000PNG  IHDR&sBIT|d pHYs&tEXtSoftwarewww.inkscape.org<cIDAT8]lUy۵ic[EI4yS9L$cL5zFLGdK4^dJH :.XJx>>7'Os~yNCchIzn׮AI///:16-bg^{!φl6K.+J^Lڹ=q8D( B4`" Z]$@0+f6hcU5Z z퐱=VbD2 ,+fO6?ޣ]Y)5U}IusssJ۝Wxp޽9¬ucu'Y1 >KODp8;T`4v@!dUA,}\3i? {=@Ram/,,,] PC.b&wwwwaUUBBnśÿ(޴Zmu"C l 08ed ٱC?_~bqR *p X!FBtߺ='˲F)dn ύ**_j~R6;uRMfȲ\:ЖIchV<7zʲ|o.~P.Wni`yD2~|dzǷvOB>L t5Xcc5YSo> [+@IENDB`Nagstamon/Nagstamon/resources/close.svg000066400000000000000000000167741240775040100206130ustar00rootroot00000000000000 Nagstamon/Nagstamon/resources/critical.wav000066400000000000000000000217101240775040100212600ustar00rootroot00000000000000RIFF#WAVEfmt ++data#}x|eYlTf`ndfnehra|UPMNwcdzdYlRoR{rYq{xvsiV|}zzshU{y_d}hTnN}sUn]Wz]qidt\ROKQ}naj|cVjRoUw}|xohW|zxvnUrxYttWexaWkRdblfdpdkocyXROLvcd{dW~rK}mR~nYp}p|w`Xy|{pomW|e_zmRrP|uSp_W\ydloc|XKOOzr_geQmOrSy}z{kiZ{{uvqVmuUuwXcycUkTdein\vcgp^{TQSPyZh^SfJnKtp_is}vumO{||{ml_n`\udWiI|l`mbz\rdjhcvXPNLLk\qlSsX|yWmprsQuy|un^`ySx{VbhUrTzbhho_udgo]|SMNO{ae~cSnIuMor_hs~wunZk~utsg`~kYpNwJtw_db}]tfiqb|\VRTIuobagNqLwTmzytkcd|uwmb_{zl^vqRagKtH~dbogimkapYJFCLyn_|jQyHx|Rfvb^|yxosTk~~ty{xTq~qXxJ~}Qoz``iw`rjhwb\WSQOqm`dcOnK}uVo|zqnjX}wswhSu~n\uzXYkJtOxj]q_fkf`tYLHOItV~oKzLqxa^~qSz}pymHx|{|`W}sM{MuXjyh]xgiiqg}^ZUNKStkXjbPnL}rZpxmskS{tt~}lOmk^w~Z\iKsOzk[nafkd`tZ~NJRNwu\oM|xQnzd[rVqyw{hTfz~|{mP~rOwQtzda~qS`yY{[URSFUWfkWq^XhVxr^ltiugVwnx{~wRoupSy|TbcPqOgbckau]hlbsOPLTkp\iPsRr{b]m[oy{{zhX^vyuqSqU~vYovi\qOf{]w_TSRFW~ZclYn|cWkV{rWo{womib~mt|~ySruq^vr]eybUkS~j_fdiradvhoVXHUgwV~wUvvUsxZgzjXxws|lmWd~uusjTqtZ{sZst`b~gRuv`t\shclYUEOMujbjuiV~lU{tRsu}sdie~mr}wStqn]xj\ev^WgTg`iifubipfpVVFOcqZ|zpYyrStxXk~jRv}o~vqXZ{|keXvl]pXrtc]~lItichafvYoXQBRWv`\tr`]|jXwMvzsth^j|}lt}tSqpb\cUlu\Y~dQlhfubZybmj\|ZHL^m_}ymY}pNsxXjlMw|mwzrP_yiY[ud\jWup]fsiTshcm]moafYUAJHhPvo_[|mT{pXsumud]}qwy{oS}x|pNkKnvW[{]SVm[}P~[~[nj]yYIVmeZkRgK~l]nmi_lpw{vW[y|zy[LlVpZpp^ghk^\rghju^jTXAIGjG{k`^rnPok\twnz\[tryweZw}sMjJwmZ_pbT}VyWLUT|g`rWINih\xiWgK~nTqqfV|t|tmJm~|qQ^wg^xiWxn[ilg^jsU~_{^icf|fzRWKfQzm^`usKvtQt{xx\P~j|w`^osQnIsobTokK{\vZ|RP|U}fYtUBHfm[yiXlLwtRkvcTy|uoO`yyk_Sym\hZmna[ohWy`drminct]^NUNxiOxp[^qoNppWxurxQd}xp}t\cw~oQgOrpZ\peIVsTQFM~Zfj`yI~Tiefu\`{jNpKwnJ\~ptdN~y|dPojbikb^~vfVueUtZPRWSziqe^[HjVzu[[tnOtnXxplwXevso~saavlThNssW]teIPpZOJKXgebuJ~Shgeta_iKrGyq[ZxzrwlFw}u|mV_qbdriU~mRlo_bfYNctdmybzVWFiR}qXaugRzhK}zfm~ejsruoca}u}gZgNvuT\yaHLp]MLJSmcdpO}Tffgsd]iHvDwq_XzzvzgKrwznZ[rebxjS~pShsb_huY~U|glifZSOEdOpTkubWxhT}mgs]pupvm`h|xwkZ}hK|vOc|[LQ{PV?FJuXmdXuZ`hnob^|dIuCkOYx{wTO~zgIyhXvl_ZvjK|jQvXKNVRfrcgzXyJeQtPmv`XwiXqjt\grnw~lce~xxi]ykH|uMc{[QRQR?EHwWmb`r]dcmodZzdHtGzrZO{~{zydFxwkQeo^foeR|nMreNhbCWZ|Ysogo[XNeRqQos[^sbb|yowXbqow~nabt~b^vjFtMgwX[~TOO=DDzUq^fmbeanp`]z`IrI|t^Nz}y|y^I}|viQjl`encU{mLviPg`EVXVyikj]{W}MePsNquX_mY]zvunYfroy|q_euwf^xjEtHjsR`tNWI 100 1 10 100 1 10 False Downtime False True center-always normal True False True True end OK True True True True False False 0 Cancel True True False False False 1 False True end 0 True False 11 2 True False 0 Type: 5 6 5 5 True True False False True True 1 2 4 5 5 5 True False 0 End time: 4 5 5 5 True False 0 3 3 Start time: 3 4 True True False False True True 1 2 3 4 5 5 True False 0 description - set by GUI.py True 2 2 3 5 5 Change downtime defaults... True True False 1 2 10 11 5 5 True False 0 Comment: 9 10 5 5 True True True False False True True 1 2 9 10 5 5 True True True False False True True 1 2 8 9 5 5 True False 0 Duration: 7 8 5 5 True False True True 2 False False True True adjustment_hours True True 5 0 True False 0 5 hours True True 1 True True 2 False False True True adjustment_minutes True True 5 2 True False 0 5 minutes True True 3 1 2 7 8 Flexible True True False 0 True input_radiobutton_type_fixed 1 2 6 7 Fixed True True False 0 True True 1 2 5 6 True False hidden service 1 2 True False hidden hostname False True 2 button_ok button_cancel Nagstamon/Nagstamon/resources/history.png000066400000000000000000000017551240775040100211650ustar00rootroot00000000000000PNG  IHDRVΎWsBIT|d pHYsbtEXtSoftwarewww.inkscape.org<jIDAT8ek"gǿ;qw 1 b!R*z2E4c/?J6=\C"`KUg43L[s{}x%mCr(_Q][~O]brJٟ=x0G4A(ؖ 1}ͷrYydrkɏ^3MėF|UUq}}Z700f^*9T*%Gc9l6 I@A@,s\b4-__e$I/E2 Ap 87@=1I^d2@0\66pDH4ڂiȃ1L&<%q2 J)(899N |sssH&WA}LS\2sFP{{{d{{l,;9p]`%3 8n!byy1c4a}}H*6,˂$I&Sn6eY8;;ilii 1D",,,8jCa0̦~{x image/svg+xml Nagstamon/Nagstamon/resources/hostdown.wav000066400000000000000000000451541240775040100213430ustar00rootroot00000000000000RIFFdJWAVEfmt ++data@J~~~~~~~~~~~}~}}~}}}||}|||||{|||||}||}||~}}}}}}}}~~~~~~~~~~~~~~~~~~~~~}~~~~~}}}|}|||||||||{{{{|{|||||||~~}}~~~~~~~~~~~~|~~~}}}~~~~~~~~~}~~~}~}~~~|}||}||}}z{~xryuwspt{ytkswp~pgru{{{{x~}zy{u~|{szyy~}}~z{~|~{w}{zxyzcu}uprkct||worfjpu~wxxy|{yyyvs}wxxoxqw|yx}~{w~}flsp|q|tzw}~xt}v|qo{z{{z~~~|yt|~xvz}x}|}zt~y|}|z{}}}~}y}|~|{}|||{z|}{|{{z|z{||{zz}~|z|}zyy~|{|}|yz{z{~~}}~~}{}~w{}{xxy~~}z{}}z}}wx~~~}mmvvyw|tmly}yvvrpmlmzytwee{{m}snkuw}zigtm}ht|w}rsrQjwgwiz{w|e}d}{nfp`oY_y|ekglcv~ySfbj{twqJgcgy\dsymhwYOiui|UG}s`hwxrldpkW^|xuwkvpdQ|afppviq`uxj`lidr~vyzx}tskhnpfdh{~roplhswuqu|~{}{yz}vqz\`vjqw{y{qt|}t|xukptbr`vqow|rtqtirtx}|||w||sjrxpjp~yopson|{tprpvpipzqt~}xv~{~yvuxz{zw|~}{}~~}|}xw~~wuyttyxuy{xvy~|z|}~wt|rZkruldut{|}z|~xou_uhfqady}yssn~|zvp{voeofwutz}xty{uqhwxiTesh_xyuqpn|ronZg|h\bxneezpxx|sn|k^li``qyojwrbk{w{nnri`apies}wtysxvu~potu_Tbrtjv}sY’ńcttt}viRQ^VVeqizͨ{avxm\M_\X[qxsy}zh``[YV\OL\bVu~kҙоsoeacOgeaP;$=b\cjӾQj{g^zn]??8<-Q{lŝxyæ͢sYp\E@mWEO`lRZªtpeXVpiXTf]at}toýUtv{^6>KZQcmveuw׻Qgq|iV:1(I;2RzҞi1Ul2LtJ(9GVw{mvμ俔ð[spQ=OA'.Y_NBWch¯ԜͅŦ?3- &4RmwǼ빖ԫftB,C002(&,LbvθǧoN.--  &HZ_Ϩ~apM:/! (>IUuڼٚѡxmD2B/*&%,>L\ߵᾙyQGA,"*" #8CPoֲ_NU@-!&->J_}ظyX?K<"+& "3P@Jfwܾ伕jJ=+(!5A>BXp͙wcJR0'8NBUp{|vUYA(-&'9(&6Xp|Ӷ繑gJ`4-N8 -?#6Ymvѿڹ಩kR0+857;Xe|ӾuU6' ;7#2M]}ζtuR:/&DeGL`~ֽ|\xZ=#<&3"8USQo{޽ĞoR<2 ,)/@RMKVoϭtxYLN(&2.V^UmצqoRM3'0'19--?_{ƿ缋`=_7(MA'G0 4VmuĴԸxnU8/ 7N24Qhpߪ|]SN<$&!-NcdgԵPZdA!I47CLVpͰxX@:;3(.HZ^Zeyx²{h`U?,(3BNVe|ƶw`WNLV_YKJIFJ_t~lPKI9=HHDNTWUU[lǼxcQB54<89KXVR[dmŸuhQ<7=2-1=8;DRevȽ|gTG=5/./129AMYp~VIN@//961)+5JXfԾheS@0.(7:86D]_hv}ǿr`X;)8@:4CHGMih]SQOY^WKMK:DlxȦ~kdYTPX[XMOdlcgjq~tzzpgbXSZdcb_vxtmc\]kyoowra]_cuhbw|zwpKG||uftjWj|sx}rpuihobrvoijjv{oivzxoeqnaY`b\\meW{uĪ{kagi^]t~oirm\`}wfõvvYUMbpcYtvT_zw]c^GPU:^~rVrjnŽj|_lsUKW[MOh|h[~zmhþnYg}iqzXEHJKNblVcgJ:M\dZYqmliZjüѬ}}c6+NTCV|~dYy~|Խpn`Uwos_=@ICWxn¼j{Xaz_O\:.\dLlcйyZx{hutpM.Lujck{M=u}\rtt`/NkLljLrsYtРhh{XisSFlicdqeye~xẛ}xcn_PM0]|jL{jn~ĴpΤe_^^ccclh^PQ[_WnͬzДqk|rUFUW@5GK`tsl~Ʈi^s|kTDBI66\g\q«~WdS*9QgiȼrY]5(1* 9?6FRlvѲܿvf?.EG6NN0,BSL`՚ݖ{ev_87<5-9)'/LGYzβḐ̌\?;8$/7*!:O]dz߼§qJ*'(9Lkɻпz]G4'$HS;BZ{ίocG-= ( 5I=DbpԮ]G50,1CF59C[lϳ_N<"# &OORmᯀzZ?".4"DG/)Edxvв靼ۤ~\Bp:&I<@)0O_mܴෞ}[153%8(1S_gʧ}\A*0J/(@[p£~gP7)*Co\MS`~ʴw|~X:6! 7'IaMSqrP4- +*.@NMKVhxٛߦp|[K@.&!AU?IcivƋvlTH#-+:15Qoƴ޻ӛxMDD"AH79 'Ilxм˟z^J$:9KR.2Okp¹ǪylK)! NT2:Wfܾ~z[A2 "--`wNRdĮʼjd]<"./+-GXGGevŶܳlF2. 51,EWKK\uܬ͝tqTA4 ..>k]]{ʬјgmOF.+6,!7E65Mp޴מњ~PFZ%0Q+0E.+Ip~ϭ͙dG!<1 GH,0NjnȹqdF$%#QO-;Wh¦mwS<-2 %VUO^v׾w^zbC: B%7YkUo⳺Ϋp`<, -94Qa[JVf颶תwm{[=G% !=#-\gWtҠepQB$/*-D44KlƵکvW4S&!Q<*U58\ptҜͦiK&48 :Q:.KgkzϸñxrQ5"#FT67Pd´z[C5- .XpJV`|ʶwv~Y9,$"7%*PaIMhʲᵔpM>0 3,3HNKN`pɦќopSF0/!@Y@Pmtyy`OB )6 98)4XvŴﴽ嶂e@\2'R='G):_vuٸץݮpV<+J$8U6$?Zhnʧc@"0 9J-&BTiݻ˧}oV=*/ 8hK:LheeH*: 0-QOC`u׹ͥtV<& 2*1DP?:Lh{ϳԡusSQ:5$AXAQnux€hXE(93(@,$5WozƴpGn=/YB+J$+Qdjȼۯ෦|[72= 0H. =U\jĞ}[<"7>"&?Tpνŗ}aH6 +Dc>DOlվtjK174/":ZMAYlԫjKE!&2-9KD?KarlZI.&(UE=^prvߟ{uZZ0*7"27$&Cds{ç̏{KXM#CN&=. D`hzɳ뽜aF D* J1.M_`ЯiH"-% /H1&=Pcʵy[A7 1 5d\@KbżpwZ8928&'ASBNh~ŞsR@(3,8FG;jMFSkԳԾfjL-2&.0PG:Tiѧ|\@8!$'4C;9G^reUC +"*SC?^pu~ܡzvYS0%1 .6 $=_s~ɰҽߧ_O\07L//33WesÿߴpS,52 9< =W^wӹmM- ! 2%Ja:DXxǮr_A170$ ;PAD`sͣyXC/ -(2CD79Ljwڧ}tXS. +=Q7Hdqs͌jXF'0*7"*Khy̼h?_9+H3"9(L`l̶ܲnO(41 :>P1.53Tdoν彭{\91>5D 8RYnΩdF" )5<"5Jeֺy^C4 00bJ4CZ~Ħz_;9..#?D3AXovUG, "+(3E8/5Ldo٣{_Z2!0CH2E_jl~kdF-8*&;-Sltǧݰ鬝ĕeLm?BW9282R^m߷vX-B9D=>RVq{X8 $82"I`ڱʽr~[4C',7!4LE5L^z湥qRD# .12?M?8@ZmxƓlVK(+(UI:Rilque\;*1(&8#4Wnxçݫ⤒^Fm9=\:/?4Xfk­̨㷠|];.J&2T36R^jˤgI##& 2I1&;OjǬoR=.7BoOCMfϰǼdxR(;%*6#6OI;Qe٪֫iI>! &05COE>D\o}궹~qWN" *7,[]GYosu߹xl\D271"2@-(?`x|Ťע៌]Fq@:aA1E5YjiΦ䶜|\74M!>V3!)!0?)8VVASmիڵpND&47>4';>1/Ig{z̛ؖUNo;Eb7:@:`fiµΙoR%D>OO,'Eb_s̶zZ50!'WO+5Qa׸eHC(57]wSKTuűttS8J77IF9L`RB[nеͧjOI,@FIU]OHJ^o~ἾםֱpTP77:lQD^gjpŸҔgdX=7?8%6@3,Dd|y՗ܚÕ\QvAJnE%>K%;dmeºؠ›|_5DQ"L]>'AdejʼĞiD!/+ PY2,GTņǡ|uWE2>"Kx\EGcӬnV'^_dǜlI""0B_>*DWqɩ§}uYD6 C#M|iJId۰ýu_.:A17G*?>'#?>4?VrzyЊP\f=S_5&II('Ihdkŷ̙xZ/CLRhE&?_aeɾͥvR)0=bB$:RjӰƴbH> 0" :`nLDSwὶzne6)G.)>.0GS;>_tմŠXF5!I=GQPC9C]t~ٲتaLHF2!KrWS^hsyБs_WGDB.*DD;CWq|}·ŽT]qDX_3+NH!'Fe`iɹ˕wX,GFWe5!?cYjĽ̿b>&(RX.(FTr}tWE02"Dm]HCeۿķu]-CB*(B)/HT:Ee̦aN9&NAIQUG@aF+(3?^xඔ˸ZWbH/CGRFLIFZinppǭutV:7^ePO^wJ9hnx]burdhf\jdURzuwe}gqwt`fzXPt{das}x|oeefmqhlk~~|uhjbNhtq}wikgtzxsh_q|em{t[_}}}|rrptrx|f\`pm`^iqu~y^Zeddejfgjlpxxt~~tadMSZ]V`svfdni`kcTMPYbimjsv}uyilurkfjpmt{vqX|uwrTR]UA^sntjuus|kjecXbXWYghc_rt][WQSY][`aYUhqmͭʯ|}~xPETOIRhoywȻzlllb]ZQQ[pr{~yjlpnfimrpqytit{m~~n}tcxjT[ic_mqbrvy{y}hkt^Gt~uyqzpl}~sZrmssh|uz|`clzqotuxh\umbvmhxo|{mnvmy|vSTbe|yt|`IXinszomw}}|vpeYfoZ|oy|u~}|liunuoj~thsxmhip~vnnt~g`zndYYtiThu`mzPIlmo{}v~z_Xv}wV^orsd^eHlx]p~`\|yivp~s||zphf~ulk~n\~}zz~tdnpsmvuop}{|{rknsotuo|{qn}oegljeentljouu{}zqhfb\\`abbfrs|rttc[f_QNV\lrpx~mii`PLZb]aipkpx{yzvogbeb][gj^XrvwlelheglhgkdYowqokhbacd`\cpqvy}xrfXRQGEYhpgp~pic\XSKRld\l{vqvxvpl_[cggfidgtrnz||{ohahtgN]~~pxz{tu~xotxzq`g|sft|m~}x~}rr{{wsmq~|oqrfirpihtxqiu||z|skhjfefjb\j|rn{yzojg`\YSXciiq{}|ypda_]Z`fc`fsrny|xqeefcaefehkmlz{ttqllnmmtuv|~~{vytnppopux|~}|yysnnpnmoonpy~~zuutqqrponklpuyzvutrpmiffhjnx{}yvtnjhjhnopuvvy~{vqkjkjlmllomr{vrpkhgihjlmry~vqrlklmkkqtqpz~~zqtuplpolnuuw~~wvvusqrsrpryy{yvvutuuusuxzy~~}}}yuvvsswy|~|ywtstpmpvkjxsxxssurtxpftyt}uxvqkttkmxwuwwy{xvuqrsqptxrqv~}zzyxttuwwvsu{|~zz}|yzzy{{z|~}zxzurturqy||}|{xwstsqptwuy~||ysrqqqokltuup{~}|lotspjjuxqqz|uxzrpuuttww|~}{}|vtxwwvvx|wx{~|yvwwwspssrqw}~}{{uttqrtrqrvwu{|zxvqssprtqqqtx|}}}{wqttqqrnnpwzy~zxuttqqqtqswyz|xvwttutuvvxy~|zyvrtrrrsxvy~}}wstsqqsppsuxz|~~{zwtsqpppnmpvvuz~}|xqsqplnnmnrutz~}zuuursutrtwy{zwwwvxwwuxyvyw}soklnmlsvx~||{yxzxww{z}zxusqpqpprtqquxz{z{zusuuuusswy{~~}zxwvwvuvx}}|{{zzyz|{}}{~}{zyyxzzz{}~~|}zzyzyyz}|}}~}}zxxzzyyxwwzzz|~xyywuxwwxwx{wy}|yyywwyyyy{|{z}|{{yz{y|||}}~{|{zzz{yyx{|~z{zyyzz{|{{||~~}~~{zzxvwyyyyy{{|~~~~|{zyxyyxxxyzz~~}}{z|{|{z|}~~|}}|{~}~~|{{zyxyz{z|}~|{yzyzzyyz|~~}|{|zzzzyyz{|{z{~}{|~}}~|{||}|}}~}}|~}~~~}~~~~~}}}}}}~|{{z{|~~~}}}~}|||||||zyyzy{||z{||}}}~~}|||z{|}}~~~~~~~}~}}|||~~~}~~~~~}}~~~~~}}|||||{|{|}|z|}}~~~~}}~~}}}}}|}}||}||}|{|}}}~~~}~~~~~~~~~~~~~~~~~||||}~~~}}|~|z}|||||}~~~~}~~zy~~z|}xvty}}yz|zz}~~~~zz}y{z{~}|}}}|}~~~~||~}~}~~~|{|}|}}}~~~|||~|z{}}zy|}{z|}}|||}}}||~~||~~}}}~}}~~|}~}}~~~~~~~~~~}~~~~~~~~~~~~~}}}}}Nagstamon/Nagstamon/resources/hosts.png000066400000000000000000000014501240775040100206140ustar00rootroot00000000000000PNG  IHDR1_sBIT|d pHYs""S@tEXtSoftwarewww.inkscape.org<IDAT8moTWsݗw1~ A$!)""RRPC2A("yt"i.Jinv=صAN9of~3B`}{sե\¾ XUڧv\tѓq]+#._8X^xmpkAB2l*AFVeJ91?ݾӧ~h0=;?d`xx. :Y/Oz}mraVMp~'ݵlEF/2*ň#Sc3tzڝ QEDwEL} KmaNNK炱-U6 [>F-8ڝ>>F Tj>P(y8.3\8nooo~ɇ;{+  xx;Fd Nrg$Iizԩ[Zm)jVTą(ϯz'ߪ]6wP,νj A M+@nv^pue/k|H@T2O F*r| X}32~0@T  \*/F_vIENDB`Nagstamon/Nagstamon/resources/hosts.svg000066400000000000000000002627311240775040100206420ustar00rootroot00000000000000 Nagstamon/Nagstamon/resources/menu.png000066400000000000000000000006051240775040100204210ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsXXtEXtSoftwarewww.inkscape.org<IDAT8Ք1J@gCYYZx )r }4iD=F@kCdFS>x?,,?KIQEjNE]' ]}ngSC<}(H%=HWmE4gYm~lKXm"kl]$4 hdQUՅsf^g q $Ic<-c{佟RPIB,l >" +^BjIENDB`Nagstamon/Nagstamon/resources/menu.svg000066400000000000000000000310651240775040100204400ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagios.png000066400000000000000000000020541240775040100207350ustar00rootroot00000000000000PNG  IHDR1_sBIT|d pHYs''~|tEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/!tEXtSourcehttp://jimmac.musichall.czif^\IDAT8eO[u9=@[V ?Fqh挚x1zh^mDoz7,FlQ [6e 8VXYh9usz {H)x{4Xߡz&$|)5q2q98D94ctwTiï!W߸zcoߝ>3yl"V=3(Jpl.F:s2cKgGouMנ'Z!9$^-O>O+4J4|)rSHk1ÑQ⭗)W]97Mb?imZw}"Ú:RrkEVŴ=+6dzQ2+'bц@!pT NHp\A^%"4Nbq8yB@pheu˲)[Ůժx=9f5J`ܽ'z9^HnXv-)+AT,[p]AfLk ),}<עRKbU&f}rEq\aAn*8gw])JwQ:hS*[6 WubHvnl;w~`2LDCL3l>JlQX䊵Q2(gF_47L1p]8@A&^뷯rέyE=tQ5!~ׅByT>d&oƝ(gGQ:60j8G@Բ|nX冷nL~}(y)ea*kPlR;iVW|IENDB`Nagstamon/Nagstamon/resources/nagios.svg000066400000000000000000001051321240775040100207510ustar00rootroot00000000000000 Nagstamon/Nagstamon/resources/nagstamon.1000066400000000000000000000027341240775040100210250ustar00rootroot00000000000000'\" t .\" Title: nagstamon .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.75.1 .\" Date: 03/20/2010 .\" Language: English .\" .TH "NAGSTAMON" "1" "03/20/2010" .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" nagstamon \- Nagios status monitor which takes place in systray or on desktop .SH "SYNOPSIS" .sp nagstamon [alternate\-config] .SH "DESCRIPTION" .sp Nagstamon is a Nagios status monitor which takes place in systray or on desktop as floating statusbar to inform you in realtime about the status of your Nagios monitored network\&. Nagstamon connects to multiple Nagios, Opsview, Icinga, Centreon, Op5Monitor and Check_MK Multisite monitoring servers.\&. .sp The command can optionally take one argument giving the path to an alternate configuration file\&. .SH "OPTIONS" .sp No options\&. .SH "AUTHOR" .sp This manual page has been written by Carl Chenet \&. .SH "RESSOURCES" .sp http://sourceforge\&.net/projects/nagstamon/ .SH "LICENSE" .sp This manual page is licensed under the GPL\-2 license\&. Nagstamon/Nagstamon/resources/nagstamon.appdata.xml000066400000000000000000000036271240775040100231000ustar00rootroot00000000000000 nagstamon.desktop GFDL-1.3 GPL-2.0+ and LGPL-2.1 Nagstamon Nagios status monitor for your desktop

Nagstamon is a Nagios status monitor which takes place in systray, on desktop as floating statusbar or fullscreen to inform in realtime about the status of your Nagios and its derivatives monitored network. It allows to connect to multiple monitoring servers. Nagstamon supports the following server types:

  • Nagios
  • Icinga
  • Check_MK Multisite
  • Thruk
  • Op5Monitor
  • Centreon
  • Opsview
  • Experimental: Zabbix

Events could be handled by instant access to failed hosts/services. Nagstamon is very customizable by several types of event filters, notifications methods and actions.

https://nagstamon.ifw-dresden.de/files-nagstamon/appdata/nagstamon-appdata-01.png https://nagstamon.ifw-dresden.de/files-nagstamon/appdata/nagstamon-appdata-02.png https://nagstamon.ifw-dresden.de/files-nagstamon/appdata/nagstamon-appdata-03.png https://nagstamon.ifw-dresden.de/files-nagstamon/appdata/nagstamon-appdata-04.png https://nagstamon.ifw-dresden.de/files-nagstamon/appdata/nagstamon-appdata-05.png https://nagstamon.ifw-dresden.de h.wahl@ifw-dresden.de
Nagstamon/Nagstamon/resources/nagstamon.desktop000066400000000000000000000004151240775040100223300ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Nagstamon Comment=Nagios status monitor Icon=/usr/share/nagstamon/Nagstamon/resources/nagstamon.svg Exec=nagstamon Terminal=false Categories=System;Monitor;GTK; StartupNotify=true GenericName=Nagios status monitor for the desktop Nagstamon/Nagstamon/resources/nagstamon.icns000066400000000000000000005267471240775040100216400ustar00rootroot00000000000000icnsis32H}}}|}z{z{yvCAƤ<3Ң80|50\b!50EgN"•5171]K51ޖf61M܊V&62ٍU75ٛŞ9AǯGD}~|{~wB  3H~~}~|wCAĿ̥=3V^accddgh^91W 62Qlnsym4 s62iWf_x PG72vGv,'62r(h~62ߎX(62ڎV75ٛŞ9AǯGD~}}{~wH  2H~~}~|wCAĿ̥=3V_accdegh^92X 62Usx}r=w73U[__dh3 ~73TNRZJ74ZSA84I84K86᠇!:BGDyH   s8mkUH!<<<<<<<<<%qa$7GOMG6"il32UCADUDI~nDDJBMЮD9BļrCD̝DDѺ 񫚦DDϭܛ DDͬĀɀӂ DDͬÀxxxr DDˬnE DDɫ׀dkdm DDƫZZV DDŪRd  DDê RRMزx DD© L*7  DD DDǀ8 DD DDҀG DDҀ DD򾅎DD|DBʫ'|zrEASɴFIBQ{HD(87689;=@CFILKHFB?<:86784" !$'*.001.,+($   UCADUDI~nDDQBMЮD9BļrCD̝m``abbccdeefghijnDDѺg DDϭ] mDDͬ[ dDDͬWxiK7+@DDˬQnlE ?DDɫ[tsdedet^ MDDƫZZV_V wDDŪRT |V xDDê RRM^?V yDD© L*nJyDDn`{DDҹ4 DD DDҀG DDҀ DD򾅎DD|DBʫ'|zrEASɴFIBQ{HD-8788:;=ADGKNMJGC@=;98784" !$),022100-*$!   UCADUDI~nDDQBMЮD9BļrCD̝m``abbccdeefghijnDDѺg DDϭ] mDDͬ[ dDDͬWxiK7+@DDˬQnlE ?DDɫOdK BDDƫGZV& @DDŪCRA ADDêCRM# BDD©CRL*BDDCRG4 DDD  GDD EDD EDD ODD\DDb'$$%%&&''(()*++5|DBʫ'|zrEASɴFIBQ{HD-8788:<>ADGKNNKGD@=;98784" !&),022320-*$!   l8mk.3333333333333333333333)R8 a9xKxKxKxKxKxKxKxKxKxKxKxKxKxKxKxKxKxKh?+tR?|Ĺu4&5CQ^ju}ti\OA3#  ih32FDCDBDGACR ~FCE+BCjOC+BJڬCB+DƺSD+Bʝ{|jDBˢz쮬oDBʅoDB{܂사ënDB}デ߂ˬnDB~ჁʬnDBz؁znDBāބsssd?7mDBÂ܅lπV#mDB„ہfifmDBւ́_q_ՁmDB؁XXXP:lDB῾ցRm9lDB྽ցRlDB޽րRQm/lDB޼RQ6KlDBݻ . K6|lDBܹÁK kDB۸ρ<2kDBڷׂÁKkDBڶׂÁKFjDBصׂÁKjDBسׂÁKNjDBײ͂ÁK~jDBֱ~|jDBհΝ}{jD+Bڮί~{|gD B泬 }|zWD+BR׮}|{CD+DCµý[C9)DCjSCD=AC@4'  "!$(,/239:?CEHKLLJHDB=984/-+&#  # !&'+,/1355776331.-)($"     FDCDBDGACR ~FCE+BCjOC+BJڬCB+DƺSD+Bʝ{|uuvwxz{|}jDBˢz` uoDBʅ]oDB{mgnDB}qdnDB~l_nDBfz wcOD8*$nDBābsd?mDBÂ\lV#mDB„Wf[mDB_sh_vlsfmDBXrXXP+}nlDB῾Rl`9Q}nlDB྽Rlm}nlDB޽RQPM}olDB޼RQ6'\ xolDBݻ . 'AolD Bܹg'`okDB۸3*kDBڷׂÁKkDBڶׂÁKFjDBصׂÁKjDBسׂÁKNjDBײ͂ÁK~jDBֱ~|jDBհΝ}{jD+Bڮί~{|gD B泬 }|zWD+BR׮}|{CD+DCµý[C9)DCjSCD=AC@7'  "!$(,/27;=@CHKLNNLJGD@;861/+)%!  # $&)-0134678877530/,($"      FDCDBDGACR ~FCE+BCjOC+BJڬCB+DƺSD+Bʝ{|uuvwxz{|}jDBˢz` uoDBʅ]oDB{mgnDB}qdnDB~l_nDBfz wcOD8*$nDBābsd?mDBÂ\lV#mDB„Wf[mDBR_;"mDBKXP lDB῾GR9 lDB྽GRM  lDB޽GRQ0!lDB޼GRQ6!lDBݻGRK. !lD BܹHRPC.!kDB۸ 'kDBڷ"kDBڶ"jDBص"jDBس"jDBײ+~jDBֱ)s~|jDBհ8Ν}{jD+BڮvLIIJKKMMNOOPQQRSSTUVVWXYYfί~{|gD B泬 }|zWD+BR׮}|{CD+DCµý[C9)DCjSCD=AC@7'  "!$*,/37;>AEHLMOOMKGDA<:63/+)%!   $&)-01466877532/,(&"       h8mk (ĀN%{>fiiiiiiiiiiiiiiiiiiiiiiiiiiiiK1 K hE $6HS^itƼ|qg\PF1! '1CKŽڿPCCDDCqMɼzz{{|}}~~RCCDDCO΀ȳyz{~ WCCDDCPɴyzz| ZDCDDCPʽyzzZDCDDCPƤyzzԳZDCDDCP˿zzزZDCDDCOɷ{z|ȯZDCDDCOǬzzɍ𹮯ZDCDDCOŦ{{ZDCDDC OŤ{}쫊奉ͲZDCDDC Oĥ{쪊榉ֵZDCDDC Oĥ|쩇壆۷ZDCDDC Oå|맅墄ڷZDCDDC Oå}륂䟁ڷZDCDDCOڀå~ꣀٷZDCDDC O¥~}~|ދ|~}ҶZDCDDC O¤z|~z|{ɴZDCDDC O¥xywۊwyǵZDCDDC OuwxuwxywnzۍȵZDCDDC Ostr؉rtuvtj\L7AˍȶYCCDDC Oprprstl\K2"ōȶYDCDDC OmomՈߐnpeS:#ōȶYDCDDC Okmߏ`L/#ōȷYDCDDCO׀hjhчr* #ōȸYDCDDC Oÿfhg@#ōȸ YDCDDC O¿ceghec|Ά:$ǍɸYDCDDC O¾؉acbowec a4 YDCDDC OЄ_`_w^` _p٨1ٌYDCDDC O\^\w³_]^\;$Ǎɹ YDCDDC OY[Ys}X[e:#ōȺYDCDDCO€WYWq[XYXH!h:#ōȻ YDCDDC OTVTo|SVWR;:#ōȻYDCDDC OQT RmWSTUK. ]:#ōȻYDCDDCOPR Pk{OQ? :#ōȼ YDCDDC OPR PkO1Z:#ōȼYDCDDC OҿPRPkŀ\:#ōɽXDCDDC OѿPRPkŀ Wހ:#ōɽXDCDDCOѾPRPlƁD:#ōɾ XDCDDC OѾPRSO`S:#ōɾXDCDDCOнPRSRF+9H:#ōɾ XDCDDC OнPRSSI2#P:#ōɿXDCDDC OмPRRSSI2&K:#ōɿXDCDDC OϼPTRF1&L:#ō·XDCDDC OϼN@,&O;#ō÷ XDCDDC Oλj!&L>#ōøXDCDDC OλK&RY5#ōøXDCDDC OκD&#ōĹ XDCDDC OͺE&V$ƍĹXDCDDCO͹>" Ź XDCDDCO͹@#T!ƹXDCDDC O͸E&$ƍƺXDCDDC O̸E&[#ōǺXDCDDC O˸E&#ōǺXDCDDC O˷E&^#ōȻXDCDDC O˷E& #ōȻXDCDDC OʶE&a#ōɼXDCDDC OʶE&##ōɼXDCDDC OɵE&d#ōʼXDCDDC OɵE&%#ōʼXDCDDC OɴE&f#ō˽XDCDDC OȴE&(#ō˽~XDCDDC OȴE&i#ō̽~~XDCDDC OdzC#*!ō;~~XDCDDCNdzl464S5565Rэ;~~}XDCDDCNDz֌͉͆κ~~}|XDCDDCNDzγ~}||XDCDDC NƱΤ}}||XDCDDCNƱˑ~}||{XDCDDCNưϰ}||{{XDCDDCNʰ~||{{zXDCDDCNΰÀȀʀ̀ЀØ||{{zzXDCDDCMذMϷ||{ zzSCCDDCeK෮ó~}||{{zƹOCCDDC Iͭ}}||{{zzyŦMCCBA-BBCDc޺+~}~}||{{zzy{ŅICDDCPֲ,~~}||{{zzyy`ECYC Gwҳ2%~~}||{{zzy{ŔLCABCP'ֻ %~~}||{{zzy^DCCBBBBCCD^м-~~}}|{{zz}ÀnGCBAABCEcρʀɀȀƀŀÁ€ÁrICD6CD[߀ހ݀܀ۀڀـ؁׀րՀԁҀсπ΀́̀ˀʀɁȀ ŠeHCDEDCOi߀ހ܀ہڀـ؀ׁրՀԀӀҀрЁπ΀̀́ˀʀɀǻpSECDFD?ACBDOYhw~|reXPGCB@@+=BCBBDGIKJKJIGDBBC@2-;ABCA>4%  &,57:;:9:99:;-<<==>??@@AABCCDEEFGHHIIKLMMLKKJHHGFEEDCCBBAA??>=<<;;:9::9:9:;:9962( i   ! !"#$&&()*,,-/11346889<=>>@ABDFFGIJKKMNMMNMLKJJIHFECBA?>=;;9875321/..,++)''&%$""   /   "#%'((*,-/02346889;<>>?@ABCDEF/EDCBB@?>=<;:97753210-,+)(&&%"   '!"$'()**,,.001234667899;;<=>=<;98 6543320/.,*)('&%$#"      !#$%&()*)*+,./011233434343321 0/-,-,+*)'&%#"   +  "#"$%&'(())*)*+*+,+*+*)('('&$$#""      ""##"#"!"                     ACDCBCDiABCDCBCDFCBDBCH9CCBCGNPSTTSRQOMIDCADBCDL[}ς΀́̃ˀʁɀȁǁƂŀāÀ`OECFGDBCG_שiLCDDCBCLy߀ހ݀ ՏSDCBDCKڀ؁ցӀрρɁǀŁ€۠TDCCBB?HDCGyǀ׀۔NCBA DDCCD\Ҁ рwGC7BBCCK4۹TCBBCCWĀ !uGCCB9C Fv̀ ڟLCCA>CKŽڿPCCDDCqMɼzz{||}~~RCCDDCO΀"ȳyz{|{xwwxxyyzz{{||}}~~ WCCDDCPɴyzzyw{smklmnoppqpqrstu vx ZDCDDCPʽyzzwvqÙ{ZDCDDCPƤyzzutŘ{ZDCDDCP˿zzxtɘzZDCDDCOɷ{zyu˕}ZDCDDCOǬz{wl˓ZDCDDCOŦ{{z͐ZDCDDCOŤ{yq͍mZDCDDC Oĥ|xi΋lZDCDDCOĥ|xg͈lZDCDDC Oå}xfφjZDCDDCOå~ye̓iZDCDDCOڀå~ye΁eZDCDDCO¥yd~zMZDCDDC O¤zd|}}~{wtplhd]G"ZDCDDCO¥{dzy$z{|zskd]XSLD;4*# ZDCDDC O{dw xywodYM=-ZDCDDC O{ctuvtj\L8" YCCDDC O{brstl\K2YDCDDC O|aoppeS:YDCDDC O|`mnnbL/YDCDDCO׀}`jklaK, YDCDDC Oÿ}`hjdO.  YDCDDC O¿~_dedecT6YDCDDC O¾gspgcfhdc gpsstmZ9&YDCDDC O{q_`_j|o_` gi`bdMLdb[YDCDDC Ou]^]k_^ ]ihr YDCDDC OrZ[ZhmZ[\\{hgqYDCDDCO€pXYXfZXYXH#:whgq YDCDDC OnUVUcjTVWR;VhgqYDCDDC OlRT SaVSTUK. 0vhgqYDCDDCOjQR Q_hQP? Thgq YDCDDC OjQR Q_M1/uhgqYDCDDC OҿjQRQ_@ ShgrXDCDDC OѿjQRQ_I-thgrXDCDDCOѾjQRQal$ Rhgr XDCDDC OѾjQRSPRtJ+rhgrXDCDDCOнjQRSRF-([n% Phgr XDCDDC OнjQRSSI2YL *qhgrXDCDDC OмjQRRSSI2Yp' Ohgr¶XDCDDC Oϼ jQTRF1YM (qjgr÷XDCDDC OϼlO@,Yq)Ofgr÷ XDCDDC OλQ"YO (Q grĸXDCDDC Oλ},Yr+/grĸXDCDDC Oκn#YP  grŹ XDCDDCOͺm#Wr+epŹXDCDDCO͹,mf~Ź XDCDDCO͹>"R ƹXDCDDC O͸E&$ǍƺXDCDDC O̸E&[#ōǺXDCDDC O˸E&#ōǺXDCDDC O˷E&^#ōȻXDCDDC O˷E& #ōȻXDCDDC OʶE&a#ōɼXDCDDC OʶE&##ōɼXDCDDC OɵE&d#ōʼXDCDDC OɵE&%#ōʼXDCDDC OɴE&f#ō˽XDCDDC OȴE&(#ō˽~XDCDDC OȴE&i#ō̽~~XDCDDC OdzC#*!ō;~~XDCDDCNdzl464S5565Rэ;~~}XDCDDCNDz֌͉͆κ~~}|XDCDDCNDzγ~}||XDCDDC NƱΤ}}||XDCDDCNƱˑ~}||{XDCDDCNưϰ}||{{XDCDDCNʰ~||{{zXDCDDCNΰÀȀʀ̀ЀØ||{{zzXDCDDCMذMϷ||{ zzSCCDDCeK෮ó~}||{{zƹOCCDDC Iͭ}}||{{zzyŦMCCBA-BBCDc޺+~}~}||{{zzy{ŅICDDCPֲ,~~}||{{zzyy`ECYC Gwҳ2%~~}||{{zzy{ŔLCABCP'ֻ %~~}||{{zzy^DCCBBBBCCD^м-~~}}|{{zz}ÀnGCBAABCEcρʀɀȀƀŀÁ€ÁrICD6CD[߀ހ݀܀ۀڀـ؁׀րՀԁҀсπ΀́̀ˀʀɁȀ ŠeHCDEDCOi߀ހ܀ہڀـ؀ׁրՀԀӀҀрЁπ΀̀́ˀʀɀǻpSECDFD?ACBDOYhw~|reXPGCB@@+=BCBBDGIKJKJIGDBBC@2-;ABCA>4%   &,57:;:9:;<=>?@AAB CEEFGHHIJJKLNMLKJIIGFFEEDCBBA@@?>?>=<;<;:9: ;:9964)" j   !"#$%&'')+,../1335579:<=?@ABDEGHIJKLMNOPQQPOONMKJJIGECBB@?=<:98654220/.-+*)'&%$""!  1 !$%&()+,./12345799;==?@ABCDFGGHHI2HHGGEDDCBA?>=<;98643210.-+*)'%$#!   #!"$%&'(*,-./1123457789:<=>?>=$;;98976542100.,,**'%$#"     !"$%&%'()*+,-.012345656765654432201/..-,+)('&$#"    %  "##%%&'())*+,-,-.-,,++*(('&&%%$"!!    !!""#$##$%$%$#$"!!                 ACDCBCDiABCDCBCDFCBDBCH9CCBCGNPSTTSRQOMIDCADBCDL[}ς΀́̃ˀʁɀȁǁƂŀāÀ`OECFGDBCG_שiLCDDCBCLy߀ހ݀ ՏSDCBDCKڀ؁ցӀрρɁǀŁ€۠TDCCBB?HDCGyǀ׀۔NCBA DDCCD\Ҁ рwGC7BBCCK4۹TCBBCCWĀ !uGCCB9C Fv̀ ڟLCCA>CKŽڿPCCDDCqMɼzz{||}~~RCCDDCO΀"ȳyz{|{xwwxxyyzz{{||}}~~ WCCDDCPɴyzzyw{smklmnoppqpqrstu vx ZDCDDCPʽyzzwvqÙ{ZDCDDCPƤyzzutŘ{ZDCDDCP˿zzxtɘzZDCDDCOɷ{zyu˕}ZDCDDCOǬz{wl˓ZDCDDCOŦ{{z͐ZDCDDCOŤ{yq͍mZDCDDC Oĥ|xi΋lZDCDDCOĥ|xg͈lZDCDDC Oå}xfφjZDCDDCOå~ye̓iZDCDDCOڀå~ye΁eZDCDDCO¥yd~zMZDCDDC O¤zd|}}~{wtplhd]G"ZDCDDCO¥{dzy$z{|zskd]XSLD;4*# ZDCDDC O{dw xywodYM=-ZDCDDC O{ctuvtj\L8" YCCDDC O{brstl\K2YDCDDC O|aoppeS:YDCDDC O|`mnnbL/YDCDDCO׀}`jklaK, YDCDDC Oÿ}`hjdO.  YDCDDC O¿~`efeV8YDCDDC O¾`ce\CYDCDDC O_`a`Q/ YDCDDC O]^_ZD YDCDDC O][]R4YDCDDCO€\YXH% YDCDDC O]VWR;YDCDDC O\TUK. YDCDDCO[RSP?  YDCDDC O[RSK1YDCDDC Oҿ\RSQ@"XDCDDC Oѿ\RSI0XDCDDCOѾ\RTO: XDCDDC OѾ]RSQB)XDCDDCOн]RSRF/ XDCDDC Oн]RSSI2¶XDCDDC Oм]RSSI2öXDCDDC Oϼ]RTRF1÷XDCDDC Oϼ]RSTP@,ķ XDCDDC Oλ]RSTSH5$ĸXDCDDC Oλ^RSSTSI9) ŸXDCDDC Oκ^RSSTM?2& Ź XDCDDCOͺ_TUTRLC;2(ŹXDCDDCO͹T,)%!ƹ XDCDDCO͹HǹXDCDDC O͸GǺXDCDDC O̸GȺXDCDDC O˸GȺXDCDDC O˷GɻXDCDDC O˷GɻXDCDDC OʶGʼXDCDDC OʶHʼXDCDDC OɵH˼XDCDDC OɵH˼XDCDDC OɴH̽XDCDDC OȴH̽~XDCDDC OȴIͽ~~XDCDDC OdzUξ~~XDCDDC Ndzo9ξ~~}XDCDDCNDz|κ~~}|XDCDDCNDz=γ~}||XDCDDCNƱwrΤ}}||XDCDDCNƱcUˑ~}||{XDCDDCNưgfϰ}||{{XDCDDCNʰ}VQ~||{{zXDCDDCNΰ {zyzz{||}}~Ø||{{zzXDCDDC?Mذ Ϸ||{ zzSCCDDCeK෮ó~}||{{zƹOCCDDC Iͭ}}||{{zzyŦMCCBA-BBCDc޺+~}~}||{{zzy{ŅICDDCPֲ,~~}||{{zzyy`ECYC Gwҳ2%~~}||{{zzy{ŔLCABCP'ֻ %~~}||{{zzy^DCCBBBBCCD^м-~~}}|{{zz}ÀnGCBAABCEcρʀɀȀƀŀÁ€ÁrICD6CD[߀ހ݀܀ۀڀـ؁׀րՀԁҀсπ΀́̀ˀʀɁȀ ŠeHCDEDCOi߀ހ܀ہڀـ؀ׁրՀԀӀҀрЁπ΀̀́ˀʀɀǻpSECDFD?ACBDOYhw~|reXPGCB@@+=BCBBDGIKJKJIGDBBC@2-;ABCA>4%   &,68:;:9:;:;;:;;<=+>>?@@AABBCDDEFGHHIJKLMNNONMLKJJIHGFEEDCCBAA@?>=<;<;:9: ;:9964)" 2   !"#$%&'()+,../1335679:<=?@ABEEGHJKLNNOPQ3PONMLKJIHFECBA?>><:8764430/.-+*)'&%$""!  0  #$%&()+,./1245679;;=>@ABCDEFGHHI1HHGGEDCBA@@>=<:8654310.-+*)(%$#!   (!"$%&')*,-./113556789:;<==>>@?@?>==<;:9976544310.,,**)&$#"    !"$%&') *+--.012344566765'43201/..-,+)('&$#"    !!#%%&')*)*+,-.-,,++*))''&%%$"!!     !""#$##$%$%$#$$#"!                   t8mk@ =~DzV! Y&,Q:j*[ 6S;4 u );>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>6! ^#m %3w q3< "l4  >ޣR*  +=Tu|aD3!  #)2:AEIMQVZ^bfkosw{¾}xtplhd_\WSOKGB=6,%    $(,159>BFJOSW\`dimquz~¾{wsokfb]YUPLHD?;72.*%!  !%*.26:>CGKOSW[^bfjorvz~{wtplhd`\XTPLHD@<840+'#  #'+/37;>BFJNQUX\`cgjmpsvy|}zxuqnkhea]ZVSOKGD@<841-)%!  !%),037:>ADHKORUXZ]`cehjloqsuvxz{|}~~}|zywusromkifda^[YVSPMIFB?<851-*&#  "&),/268;>ADFIKMPRTVXZ[]_`acddeffggggggffeedcba_^\ZYWUSPNLJGEB?<9740-*'$   !$&),.13579;=>@BCEFHIJJKLLMMMMMMMMLLKKJIHGEDBA?=<:8641/-*'%"   !#%'()+,-./011233444444332220/.-,+*('&$"    ic08 jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2d#Creator: JasPer Version 1.900.1R \@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP ߂@ }Vqw1}Riŭ!x !lelW8pw)Ϝ< 듛x[$??߂@ ֟twë t=ä\fW`<:m+.>*@tn u񦠈ɫ.o߂@ ֟Z\Wz{8Sm]o$f zOyIStSq6|e,0NX~U6߂ V0;`!*N%E"ܷqdz }irx6AP`ba\_Z؜ߙKd!(ze(\?ÉK{1;p;Vs>G 6@q^Ȓǣ4DV;  }x3ҸpBY/PBaϔxv`2GwOvߑ{SvEK`XbmB$[uVa Ց9E2w_RF@x$&$ زy K ٔbhPr%{*R&wmxY*H+~ _愀֣7`Fkͤndǥ۰B,hZst':h,AU$SST\YXhYdo,wz&@U'y~c)6} K;~6ׅٮHֶ߷M/s{DHNqD4ٯVx[|;vhìp#z `uLA>I ]x~7$#y-ScA(Zh?x u)NkiΘ]2ۛ)@tdj hT-RMdc&o²•ݷ;ob+}M./&r=ْ:"qP5n}!11TgOKTqLDeXD&9ivOgR[$dLQ5C si_1ZVpZ_$A'!T"TKui z1k=[UoV"47 68jGhd}0"wsyi@b!R☠˼p٭ 'rWA2j嬄Ί:> HlN["Ԅd/yeA *“IU:Q} FaUT+-,0ZoD2Z O몘Z8\ \뀈ꕧ=ӄ夤XqQ윴]o 2CIXNl7 Yrp`ĺ..i*|.Kf ZvhrO)6A-U,z@ĝvZgxlc}10 AkM=N{p&wW_C=uLC8;d܏69 >aP`]uF*66 M:oEz7<4&N8' 4ac%?4|7Եâ:W֎V>*T lvv"Đ<ʇdJ~ q`H(|#>яk(|3Ll@[ZG-kawBD)kf՝@duq3jI֛MY h1J@< EɉkZѯK:X!=`W_Fc+|=>o%^. .wX+ Kִ拓=4Ҕa~"0̬Ke=*{'QkhB Ccè}o[|1kbAm#k\zqh{PUfW `gwS> Ʉgm-ݠN5Š4`= ݤH { qĕQ!dt*I aŹ9gM%5gn),ȸ&m֚W᫘xqPKOySdz=4]|h`Y>iJ,Z@BX8hȶ/D*q̸hBmCwC@K; >iyQx Rak:8IeE[h,g2bp{;['Kf~,h؜bk\n\*eo D }2,{fȿiAB,7ʭ>pǧ6bT80Nk pH[ʒ '/ڵjJ43 ;\z@wy>p|slc"tSH,l Dn#nlQ3[bN.zӳAcxK9KC' kb4dY-:M-bZi~i>~6h2Cr&ͬC.ǵţ*62/#7 Si"1m(cmw'?ǡ攷ZW[A n}oج<f9|34jz g6cMeEih#s\>"UhcӔaluRcl/_6{mCovH} S\嗧 a/ ̛5SƔ8^]CdbMr&ZWP0u_Pc=M%*վĎdB$ m$: iW?bݩB[r2SZ4ٯ!;UQ*$k ` ~귖k 8[~NHxRP{tafhUqaQVaQViβfP:%+62>yT{^?:+`O@9#Hȣ{Ы5R/eYd%|/x mɊ6& LYh׼k>Raa\z* i`@L 6Dh=7Ƥ1A9dZ͗H3>_o qm8.dJ񦁯ҕ/CO V^7.}q/ꗮ#rA;iunȡ@9^g66ki0Y?(gyӝi<4a*Xr'kY+rs]nVU3f{wTȅpJ:o*d3u%,Ӵ4j=5@~am1癲P't:|۔ \b6h Qn-UmYfgnT=Z^G4tn.JhD&@ M1k&#Q#a5QȒ2p2r>1ikB²7g.+iFL31- K*ƲCg@w8z4I+?7Z֩H ?̥P/Pܠt:S Yn~理]6w2{@D:wJ:VۏT3khjc֔/8)gXn}^?v_N!ݧbc~<l+9|-MƩ}!Q6t鉠pN&^ayK`Qq- L5 lB?/Nn˟;NF"6 OxЅjf)c9DTL2:E"KN!VR &8Pms4d-r|AŌ[SEPtϞ5nYk1aI0*I&O+ }|n~`0d+3R,aFh-"~\k5SD8eJsFbv P+IP(@1a /ۡ4?JWWӆtW>2̰MhC]b/;.+A3hh0+2A嗣Hc1`X(hTm|]cXl@eul#lʘSG&Eu t~i\0HY,> $6ȜW˫fޘrB'͑`tl6gJ%ZqEvL@ g=5Hٯ!;i)0*hS Dw8>+h$b4SK'\Xi7}"M@;iT/EL O2\`͓>wPb'*Q; ܢmkإosOTF:lչ^Ԏq֛m.<6.ew: ]@5L1 F䧩}G$Yt#/2Ds{@#nvpltB*!5ʟ.S9A/ Q5O8ڛ?z)W8 jjS D#k#ƪGA4;#sn xZ :2bTc"ozuYh4\dJ/.-(q)~lQ]F7aD).$V ٩5 N`[0 ~4JaS tԚI$v:Zg1/5bi>ߍ/ Qnh(_*dCFuyQC'lVOAN}~b87dSS\[yhH{aKƤŜ/e¿ @[p&ANd,]~8K6*(s:$̲@Et=Mƨ\ bFi -w.j` y8{dE}gG*jՒ̔D, f9xR곘* ?3)/Y63_Dd4x7D~5 %Zb]fd|r+E]hǃ& 7ɕvD&! y6Z_U؏LX[, nʈIPōgcfٶ\ݸbkT*u˧P:xQͮzjn"ʣoź-q6 >윎% aA{嗣Hc1`X(2{H4m,Ä\Zb,$*L۽-`"C =ws 6 ^BPLN6izKnWi0eH e+MsQݔf|7ʄgwA#GOeNI44JY(]qs4\T3ɏ hJ3569];t!݃!' +&k<3YՀ:*D_K@pm2).-<w"aIp]H.G,}&lspyk{d2ט 켙_6 ZT]/Fx k5$`g  +ϕn:i$I!5x .&nFX3@sǃcH$!||[) 9b<% f2XM*`H]O9e{KKS&[jb5.hOK-TE j\0 nxu>f'fků 7+)'©ITw8m#-nA) Zy/^}p3WZXbhӬsJB,N@_~ߟ}0{Q@]ٷY=EN%( s!qՓyp.2g./ C0D.Y&闤oPC9o6`Mw20{$yC^#㻈*\eA}j D}|/ ! gxH;E%֨_jF8:w\* Z[u$2<+8{/9x,"do& W7ȀIRkvKk\hWqLcs{:L~dQAm(JToPZM2#YyIzOptOΌR2Dg"p1ҳ܏mP40*JboxS lu 8 A0e'Ƅ]"ζT HEesĢ{ `nAp&V9#FGĐOF {ڂ@e -DZ]v%s|rCW$ۋp}XVPG;KFB0E\sאń_z"j#G/(Ψ%B>̝ aɤjZ>hQ* ֪A)G{%J־<2Sdh)#fW#YG#P:n1mB`.ֿeVإQ^eDD wr?xwLD8`Ы#'.OG>/j:IZQ,;RopdN*N*C4 YFӝf-R6o?a߻r靋|IY2uĮiwP/`Oia:2E\ʪlTة҅*kfq8l 9:rQ̏]&S輨?ag+"dIIB?'w J$Bu\F_f1-1ޏ#LP@̝2>:ʅh杷}LaPlzM;Dek5r5wBiA31Vs>clK]5r(ʛ)!Yu7/8S"εm$wm%.U5Ĝ_7 5+f$-AID8R^d8"<[;\V\|34V/mOIMJ-hpysA* qX+[ud>&Fd ժXiXk;"6ܻ_|-4eJ<`RyZ"K 7%MZVRs  &ls &C!C]>0-ij\jOpt Rfmx>F'g'7̏3ZܼAt=/}6Sg(KZf2KaѺ?aW.:7O^%{Y.÷si Ma@4YjN57W BkOД4Fݡ or_ Ugj2 ) 'lҜ1l)y@q~O&LITM4& iF 1s`vgRaoLH)?Tk~ -ׯnpW4pȾ/LnЈۘ*j5<@k Nv87itǬoE /Γ)죮`Y{FiYΠr5C&{AX-|,ftNPpfX&xJ-V.fCte| Y npgnq ?&2'sࠧz_ e`&x ˸eQtHk1 TMBR$J%Wn5mNV8&;߿Sƅa# rIVC>/Շ6"'`EKQR`̑(m|/V%i{bV|SlbcaŵGj igkWf 6ΦSR`8k{lZ'l'WJd""StUeyE4M3HIkj4\ -KVk\=$͆ >=,<sv{B*j$>Ix}zeϊj} GE1pz6qnrmm_UmcDhC|AȠXH~$GkHb\ysVu,Z0mZ[,p/rýXۂ/ 1Rčp%p@]w"yE+ָ:^$_SyUj,f&PNX2FP mp4Oj_N BA +9σ f(&H*>Z(3nh>t&Сc!E<9)g@vSXd]f˩$ +hRf?PZPbLd)a2eu"xc YuE&P~3hA) {!'ya?TNv ?STuw Bqt@lYz;љ"_)k:'B{u0YWqigoL}Գ僸E00N"!i&yJ!RQHN,NB46+oӨNVj F2TщH}ҢoR  kp lMAӡ3w_~8RJsvLZLOazYY, $` >l<݄b+>:GR([LB?'w#XG{qIJAOf#e'MjF? Y{wߊGPA2y }K<(h-sxT=:As ؇E]kyaƁ :q5 mcʽ^`8MYmPǒN|z񾚤LBײrQ?|O G,2zXY%T]3K_4;LnH@AdW%YEݾ[\)e/g7Q˿hQwWGWM>Oy,zcCAXԨ>80Z#6أbo-vbj9O8%XAj$zmmmmu)"8C#K*9FS + F+Zrᑳ)&sSTqgy<E8 3~1ӴW<rk^~lO wyBM'WfP->l񌼥mF d?L[ QږZ֌M0"ni!A$l,Xs= $ 8tB;&;Ef:oIy{pdD{J͆f4U i  (LO~?M@?H%C/OJs11d xQe9XtSDzź_CC5OKGfu䉠@͍nnzOXqu xu2a>l/]rJeW` Ts |0Ԯm°zo2q7*f#q\a;F ɨ,Z*ӝ xSO<3k}-"Y[b̓/OȰCΙuf (x2ǵ B}JOsB-F8e2++wOUNP2 |FpjIRYZ>aQ4S)OUg[#P(@7J{E<3Ө{[ S..MktV7yE>r2JSkuV`k&e)CG)߼J%ئ+)9om69 ȂLEw[uUMA!.8@~d7^#a']cx%(Tn2>'߼^ kKwrҳU!95**e}^xZV-yTP+d}7fchN>+X'72u 2{PS]-•gB g\a,+&nBbȆuvfW6Ük]C?>V]w^ԫ P)AtT"C* k]Wn5%j,! nxf..M if Tb°v%GOJq_YH-ke.u&?~'m \j Ydak,k6-v2'۸_eΓI!v8tQ3a1FORAnAx@{BwFÐK߷&55(OKC_E'`AFzwŎ3n禺?#euA k2z8!AU~ t:/We5ͱeb-\H-Ws7yĴWh:Qs"YC& 4O|b8VHf{|yo`(I}:Y|\ pm"O a#!81Y ԁy A$Ģ#$nyH{O'~\~kSldVk-8!sI],]~qΏRٔG0f!͸ZyOd}{;۹A)r[S(yoZPRޱ1ٴ6SP#!}lwJ2BH'ahÂ=Eae> f, p}dfZ!mnԅIs3*G #8L {cX'vނOh>s)e) GDT(UMD*AYzT0](Sq+J+irW[M&XY[uz.'SxRp~ |ӏ/,VFa\`d'N4fQQ:q 70T`<5mby 3T950_D9n@{h bz\(` +9W-"Bz'gANOǸvogV1:r QR|ɋW61_4Q8?opְ0Pໟbkr2 ^Я:^/Hi8vWK 0< zp}Q3aU.&Fkx~zWC3Y$k.$X.#1<&,_g ܄Q6¡_H-]F(֩Qv@.k~`ƪ1 9k ɾX\22/DL=0(OCw.sxvmZ߿ ϒ%/}ܮ`3;^[K7Ӹ7~9&;wSGl5B³ k6h(/ 0M VP3v3-NK7ajFiLE|LhԵԨ#ii*Xl}Նť gMHP{~ɹ=@|^477B: ,N;'W8*9/0+#" Q,eİ)+0TjɢH'_&KðX,m|S~RmmmUנ  GZHzSU bGc,~]0z._|):cE+\XR?P,vxUlS?D_U2ےI$I$HT hW 3ޖye{U—9$51g-X;>!vF;ˎ ["k^KZu?|W# Sh ][@ex6 @ Mq1'0 J&+ W!`rv 6z) UQvi;n(Ğx׷~E{7G7iJMcċ L2 6!<5)-0]InQv4og X}4trU m@f>>hdux'}@-304n#&x`V;0Νr%`-\unMFy5?dM_H!B$r[եe'><ѝ- f9Raq=5Iv.H$2֧La]Mˈuy_\(b*o%xY*N|}kA{wʂ !?[>E7=T*c|O~+]XbR2`= _sgLh`07X$gDYO'u\!43ycu *Y &ӿ2oEdwܵH/PqPFA l܁ XrxZ>9I IY7wZdMyʁ{88g| oMCa94A^'4 R `}WMaW7jq8ߟ)Jd21_ۙ>7_b-(޿ء0z?~μ3l횂F/8Z3~DEXEqm$N/QV=0~Ks9>,1}Η`ܑRA-!o#~Ȇ@Kˋm ] {*P'8YwW@dz J`#<~"yuڷf2:xfCfomTkDIŴQ`>%,a3 [o>yn+zO?(Σe<RP^YB,^r@l2{{ٹ&Cg j0W~'  v "{N'l͖{ a1 G?AĽ塏$AU4GjVͷ* A2`dYEE$B$ћHY.20K^R򗔼㙃oPFyM +7x&ʧ%/)yK^R򗔼ȱw^csB6YT-][Kium.մU a A !:/A23צR򗔼/)yKU15¬dzs'Sth(:pIG., TDŏ$ QGfQoȞ_~EUq*=׷+9i%Phv9f[x^_ʏTazMzZ9c\oZloF6?(1Ik,ҾNV<&= -;͑x>@Vh`jأ?3؎赪?%`ރw)$I$I$I$I0+W3/vMW:{8&bA&gp~yQK1eTZԌ9n$I$I$I$I$`ҦCrmd 0r:C}Ll%*fKO&=3e fsmh>AХ? S=U b2qLŸY:j[xي 0m&nی~ݮ,H-^3Ap! D_AL[/򕏜]zwkmI sRgHhTFT̐";imm >;}5oUa?oNEuG}][`[;n서},}h1ý/E=]iQ0(CѐM_ <:$y(jqJ&4;G Dabv1wU}bv%39J~YT<>wbP]ooj)"ݪ 5lj"Y&nCϑzT` SQZǺIW䱀Bb& 8(A?]Ge-72T-D }D6OoJWgI7HwNTԈNF+H4/*/D8$gf?eyZ%H9'0*4abEA7j׃Zi͡}uX'[.~|?q۽Q~)- X+vR kry0a5н9Pܖ=Ybu8$iid9BI :=SCձ7HG>GƉŀgCmJK=xSZbipJiEa6.7;A|LDӺuS~;}-r PIQ낰H ]@&(\< İ;a*aai| H g>dAI%d ~*:e$C#k[F/_uh:NUODPvlrʐу1H;nr `OLGC: &xhFL2Zqƅ|R#,5t&t.b[$]8yMIq7C .V[ZP @$vJe76F1!֜*ZZya `Ǟ0~`i;,>Ed|NRyNJq%å8_&t0y. ҞA "̿ =@ ꁀ@=Y<0 a%\_=n{GQAXt+>"jF&ܐ4ٽy8B Y+pp M}}G1Gj@dz֢ɶv&: %q$nnL^;jcKRs2Mt"#oJ=S訠u d7)m Q7\UF :/88 sej?.[FҰt&d !NbZ.HE cHN"QpO`ǠySuX!a?y\OWb T͚ͣ踐|n$ '˴7]~.&]BAH@}~Ay`#GxMV7wiS\ Z@(̠u;ַ^ì!o'"Opef4Pʎ-*Ju-Z5G0 1:50;6/ЬQՃB!A_lܭܨf 1TKCD^aZӊ[UpS1F5҅egs\kҚcpBdҬC aڠ`Nd{2}Zd/btz޷ȉM'cȿ/ 0lK2k)ZeN(9~?Tam6?W;{cP;n$Aа#e+@ҙ)Οcb୙䄦{35\DhWj:L02woĶv%E{%oޠ(ef1qnJ[4< 6+QZottڹ J '=a!7izzk$᛭5 4eTl,B#.z_Ͼ6 GkbNeً zX]%+ga;R8Oۛƒ~'3kl*{> n?J͂ l፟6y!Ge?1apFܑrG vXj n&M **5 |Fs+:.u`W yQb "_U_OO'Hc:w)O#BU" N\/; 8bbw͓/qe эyYAB(KBwSCoO%tB|yCnٱA:2wPyS2j*?̣%@ߪ 9oxg~J\,9}!9Gj3M.&!( R4Stepo!{]XrvgZ jNl>@7cݫYU伎ZZ bQ23 By|gNUdlTH2xMȗj^ ZO1*Cx/݊+|ű룔D, ⊞}D8)\ HLʅ%C'L8a|`BA,^))ڏ@[\k4%*3㺈N3 0Q9+و# V7$,?;JxX=hAYDp .or-韭2&<}YXk[Q׃Jd:T vFolѷ'fn8*@] " &5yj]~`F]$!i5ZkrZH7!btgd_\n0ӺLE eZ'i3c0\%PfH,SH6׹A U@g@&>Vu,`l2P/{CyTrH;!B''ǐI e!|6od*N#6Wjś06`3~JvY!hyV7&v .YȝJJs)n?ҋMRd=Ғ&0X/6•7DxK;cݐl:Qa?>Q@+q;E`Ohp$E ɔhdD\/nܧULs"?YlFǠNYiABK*A-̢)>YqML&wyױcs~#BeHlzT$W-xIj^g*_"d8UWZ;ꗗ3m-D-#zy/Bod-/b` ܻqiw9fzPo[!9 y!,qP`^o@Tj!V"?u,̀ON^ '" hXÊ*&O3vf XE>zz߁LmRP&b4tq![wJ)" ,~[=N!yQ&YyB2`6ڃ}ņS(r\0G5r~UO,$&q{(!AU%|)6zF,XM6^Z0N;=ReKD0,TFgy#jq1s c2M6 W|!Z/cV H.hnÁח G%U=`E!/2i^'2S.֥Щ"n|ØVgU.L~`:∛{!`m#w۔$.}@cncJjbe+f!]sy/2dNђ5C˞DLz8(|&7+i(,bbðáikV5̐e dVHQUd~ΐusoHT}J~yS*vּށs_Ȉ]! ,*d ~v/JR1n8: 'z+`+ߨA.)(UyHob_l9jp"]UI!Ti"숯dqy} .VDZBSGtIP5K\KRV8y}XZ'yw{6}J/.n[hxMrXV}rr K,@%W.|TK\A>uv}4hS<[ͱ#N>;ayZ1bFMn_{Hz#u_RORY{5N/Ş#9ZQ! ?/=H5F~;Lb&Aˆm;9ngjOH+)wEg,JEuB9d!b8-5 ۴ڲ;3.xdPDf)^܌_uIrRTzgy[lz}+.9!>dAA.P^$I"ȷDGf-$\Dj K0JRfDёou:%ϝ$+_dUĴL1RtNVê#׫JVUE9+oWII ٺك²&8Qxؑ {3 lTbT5HU{>:RYKE#) :,v`&:*]eb+s,RV xs<ױsyu9bnjYyqgz#w$N)6w_"86+;n&rЈs/?4)खJ*gS z)J4_$Z^9h[mM(r|hq&^81۠T7O-C# iG[?icr8"Գm2<Hނ:)Y6&!5wlt ^Y_Z98ߗ~5v//UTj[\*uš]oc:TpثQ>mkaȬKf=q-c%s7>Z"r^&_QJ@#ӢR& _e0٥cE=݋`e,9O"*]X/m=v"* ՜6]Ȥ9S3upɸ1أ+p9>m(ZP:1/̋&&7݄3_ /(- w_>U h-잙i^^~0@Cy,|iZrlgg4C;7_;d[.qJq#ҋֽ+apf] d(P/AI='V”_IKj4EE}`tFqiH(34&RqeK|Q&QHV&Ѵg,0Z*lԱ |qܢЄ!n2jRI0-RuEzaU$f*o_>NEb)}wrfj/w^p ),gmQ^@c%uwa9h[N&Qu" @%$|ڼO&thE}#_"FOИ"~Cu~h(tj&wyq>مd ׃!*iNа9j*=tͅm=x݁0jm}"vw|FHJ̺)w Ko U:(e2VKmW:*L3}%SD:2yezT x('Q,Eu_q+0-0>qJk p`EU ;JD1jQ5%VO)B ~g5[zoRۖ4YgmrGmBE#}cs&K-Z>e o~Vi ]bAFS)aofƴ𓭇 Fύ0@Ayǒc!)G XN׆<6IG6w%e·p1L|F2_ވD_Zyk p7 oXkM}d_u>8]=ljhksfegA0nte}ɟWlJN"үQ1ˠ _ϕ,lUxC7#RD"mm&Z2rk:N"~8mPg?z0f'SgO:i3fB-:.nиrZɉ Tg1rBVq pM V@2_pd QkOh*\2kƞ)C9v35CQȥݘzW~4?<4*k [0$E.x[BG.h%{*_]Ҩas  akjV܁w, F& _Ug&P;s."<>Ԧh([FYDZi0BT Q@&Y+e/~+1D/e82q\`EEUҸٺ:3b׿a֜f}$O!f, ~v\ 7s@{Q 5 r'`^# +WuRLvxw?#2g& -qLZ==+m;@({7XTFTYJ;K 7^ߖ;pg PPJ;AB6N^XamwppFS2Qˀ)뒂hs bY6N :2 YYU,ZZ:﭅NaQ r.D*zX*ƊⲡR7o![J|Go }Mf{z|z&5)8AP_m*yvܗs4s$%+"`|HobcTiF gaj-.$),,F luhNv3Y'!?drAVVh]]'tY¼F lN&WHq՗R))wLxS[w^r0AKrڥ|`y+ M1RWrYb?J3%)Tb".1!tѰX4JE#y.+ְkػieW&Ow0 Z6H 3*Ću{'GuZYj*=Psϑ& ͮ[e^O3{8s=x; wx]vl mqܰ4̞2{M [ 1:a!eb #LbPMpȴZXH0ƌw WC1 72N+ͩD…10g0K  Vb,SX 凕wY_3")}w8Le7<PM|G~T0dpveє„bHVT2Ii 'U"HwDP} ljQu.vkїTVlӦx؀&VqFTqt৔@5adFHJ3{.ŞQyud{۹YWbsCؙG~VU1q[2T0&Uul\P ;# אkGX$sC@: Fk sj!Elz" "xrQ1@'c反axAbwiEuL8Q )BdD"CcIGrؑś06`3~J?F{r֕Q49ey̖/VQӈ@"m'WoKpcd tdzC\ 1,rjaj~pV }7˙E181a]D?p>Fͩ aS7)6QVQK1;[s'γxzl1КhPViT>\F3wx7IWN(aJ ^-1.8I)1u;,nocިLd;[# *f#HA1[\2B^{gp:xc#}.#G?Tu?7]5< y݋Po[;((-,<dlz[ka`Dƙ#)z`F_ WCw8hk4V<QhhΊXp6 I>TP=/.E˥! LdU'1 }N2#$#ŇBhk4X8d|ăi A(gFQyWXay;.nvJP U.6 ($ yLmI@(bf=P'{GO+}}l7\ss.m@䱕yx!`,,vE5?Ш\DgƆqӎzhB;>Gw LG:rb H.hM + ̽b !}@G;ҧQ r N_k[+6sr~q $֫>dQyZ+*~d⮏NΡe5koў50ȧ⿁;2Dl$|9Kf#UUz!:B]Yt:qt&b`Fػ~ |x(; ,2:x/wʃRbD#u~'fTOcX/V#5[{tQbd/#1כ8p2gW?v:'26J _iLau( F[u>UDud&f}C$G8f&cJF.m C|RAQA%w  oݔq T%@v~ (խ6~7/&ۋm ͮkoF&pO5 whW`+ p#=2# ȣKdzJMudH79?v%l p'-OĿܰ}ne4&WoVيڶ^f.iԇF"$²{y2pdߦ,1}iio;:fX"ZW_A4j,mq^B;߉ S}%9~S`jI֛f=Za}Z"|qv13\{en'Cv:WGf3}A7tf'1B1Ŋm Ud@E[8>KgWDZ!KMe, H![oq?28 N+?ÚƮ2#›.&ўy>Ec]W}] _WI?oDwu}]-Zm`섢HMh-+QDgv) $6h їB9d맓Z;!5T0uOA"S/DzIXxO峬?LCfw5Z]n'o?0aŒ ag>XyR42 w2hA:r?9JJ@$>| :J(5׫6qVUET*ю`jMDs`ou:%ϝ$+_/40^m' 8r;#;KA1\(E0mi2R +-7=e&<&('@WNn5tn`kut$psu]Cf1im᫁-vG%; 0.1 l%šLz'l.N%Xrobr {xdJfr(~ֿM%DfcYvvfE?खJ*AFCDY_8FH2\4 yUR~P] +Hȱ8W$=am\bm_Ywv.B(lvxVc1tecOA AnI2_y0d"me(IBv ҵb^-3e&S"[* k`R@V1D [KUm IUCGTzuqCmlbÒ=<3"w #e\V)^= j_qly Գ K sD3ZU&sӭ> JEq*a%#ޒW x[3׈3>D&ry4 x{`h&j`@c_X4y%P=i >i.K- p*kq+ .2<2z^BPcqy26_pa1r &ݹOb oKq#-#9zܦ`J/1$_oV9b<L< ' f憯X݋!EhUS?]lw# \k}ʝt I\3n;7%I,6f2HFcE>ԜitNX28v[ĺ?"R!! w[SbCьÚ~_7>V`Ry[kEBT2[կS0 fu0:"} Wx .dA4c2{cr:e \NxDҢ碊?aPwD$8X0+%S%N՛BĉUAs>$s!u:Ma.m_M`W|l=@CiA.pǶjGqo8>lm }>ܗ lG\p ڣ*_`Hȇv‰-g[w͓/q ,nsWC[J Ϗe~q@odP'T B0s mY w* Sv۔AB+5HD 0%ln+F3|!(b_2s -jzUeoL*u|vU Y~nSD tỈs -wiN`>TG_(ЈDJ+'ETu ƹ JL"ש4.p-M;H&K,vR;Տs.޸+WoA^j ]>5"7 gۻGbv8Cx=Wd+=ILז acZl2T?|LC ~ֱ!g;~X!>(?T};ZxO;wb i@οў;aûf*C[Z+cKd:T vFol{表{\a+sWS[X?IԘh{ݡ]8%W1 w?$9>?bf; x`NcĨS4Ks4e4>[0[)imd3w4}`b ICJw awY_3")}w822PN~c_ρ 鬴Tjm(gl).$/i_qIU GVo޳+ D]Vy%ܱGOG߰\}z%/T-qPw&<9 z"e'⺕T^ro0cKa[k9bH.4D1Xȍ:/AP)Pny&hnwd ƨ %=\L]?; R!t̳htPm5W"LMl4qe.zf [# 0s/\nHje[o&YXyk ͜p3S eY:F_s`KĔ/)2Qڪ,+uW2%řHK9;6=aGrؑś06`3~JFypCKphJZvzRJ6=`RT}ߢ-MxlYӆ4맯N[@ņ)yB\<  V6i5W5%zKYqxY.~Zr^1h""Vܝ%jK|Rm2<32O=u1$q]IY?[k_D<% 'Wn1da{ :Pk҆kx]C(pSbn'2qI%ʣ&/~q)2 !1ElF l7tmÁ 9UuvS՟ k=Hr@~+<`O 5la;m ׉};Cr]e'W6 Ox ζ:s%@(ŠC\s`J$pڻ$-&$;b>yФo6IҩI|C4u.Fd(]R @Ja@% >ݥi!n.]cJ'~j 'Yg>$$\e%* GiRBͧrZ4F%/`Sm_F&\(}yRm2(< JLͦ H`GPi@p 0F;;k3P6x|IG{^9F4ƋejV܋uWBlշs3`Ng18(Swt'KN$lEL:(gW%S "8x57$NMTP>LwGFD(ʬxJ`&pe+& ai5d6pDɟ5&=CCukI8lH'.k9,?;d&ٰ5 7h>QO Y gh?@^gW(%^___4K\R@0v*_L"9A7Ų mO"#|Ja 2DFb5^ʶ"~ѷ;:q1Ntd;YR0_r9HǂDrJ<¬x+{# 6W@f0/ B$9ףŸ:S3QɩDdbUDI?evNylQ.N\s:8Z%^LJٴbsJ0%cn`~spT>t+}u 0'sA3"Q% H.hM + ̽b !}@G4gE-sv(.:=VLeBua J +~A ol(TP~ N,N3. Z.)}ƋVZQ9hrZ=*!B7@`rJR @vW\*Cs3xsU&uA6i=MU`{$0 Sh(3laϕ06a1ÏuS .uh ^xtrw1adu "|f:m4$)#"ȡF"0zGY{Iґq8KkqXGepM_@f $8Ao^բtIP<뤞IvjzB4iK[qJdT?'=n͈j1H~!g#Fz|ӵ^9qx07-%1/Yk_DS[꬘%^]|t ?𫄯JI NzpI^sG 9 U&z? )bRHٔ8n;o6dᨘ;21H~Ň@)MDG]Ge*j-c\Ex"4LI!qjK K?¼4yv;:-:iܣ I g|h^ hqsoyyX@KG9òOm=kȚ[FA )%: W}w_gr< 7`aT|KcDliKkw9j4UGYnR9J n;á vp~j! ʛ<@M%:b s3T(oa#.y[kO␢ɷS`grgsD(czʖkb#DV5[ē%sݥZ4ZHͣ m}9Z‚Uz;5Q4R ͒.OuvOuBG,g;<ǭkӛg)Z Z@zV<Ƈ>\nt> EN0X1"E'&b@Z%P ŗ[wiO|s"mj7bd]E_hT )q.ZժL)nD#W:_ET߼?^~w?Q2TA=ۿP2\q;m|7y!#{,!i{Z!?3-bvi yIQ!('h)*&yd2F&?:UL6^ ՟ xToo$/b.`d[{#ZqW~or7<6r<On;;Н,|dx NCmT!ua[Z////// >Gl`y XUSjChVȈN~+N@'Dm`ҹ.Rmx\jнb1+s*B2,y頱V^5{4t<r{ Ū7v( ѹBFfDq-X/qԟ>'0ǵ7s*6d;;T?uWˑeRܓ).p˜29M]tSؑt}4:!()Kdu1lɋ%xPiu,2X!`-\JM!퀑{=O^4W3RWu-9s>;H;JP˺b|V6c p1l ! 2@G:Q[1`jݖ5ڑ:eKh-6ŖKk~AY=$Yb\W)Guג 4s¢KעzY>P7(zYS$gƬZ ^ ΢pMX;I<x~uEѱu~2 ug{9b ,__p1-'nF׼%Y;izK2'*ڝ ^jiN"+fBx*g[kؿ\@L9]A>p}4$xj87E>Rs?ӺEށSL ^. =/MEVrXNHNqpZXa vv(eS,/BB$Lϣ.4sOuxD|e&OY%Xn! X;&g 5U, I4.Rt2ic09T jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2d#Creator: JasPer Version 1.900.1R \@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP]@@HHPHHPHHPHHPHHP Sw߅|Xڝ77SY$dtmD*) _KGO2QvoT6ɀ~ifffrq`yS -7qIJfa $DjŮ9#?º”n;C6Xjbלeՙ SN) nXvk`0Mu0KG|dA&:rGY\!ځAn ieYx Ɣrc |Tcsw; Z Xi$jNmH GZ@=AнP-b3 5j͕ʒH'[e7&q9|/|F{0ٱADU狳B%Jg,hst8!L-.XYݱ.DŒ: ?}uLVt❼uW3 w%ySxPaz&bys,tY^=H`!@ t Ƕ73B9gb<j8) IHIfj.vg5 W.!oMm qLDFfڌXʠ)8ɶ3IT&+tHC'Rzxp 9#|o­’c-R[ (P(UƝsJOd l7fvÝ3H؝KÁ[{!)[-# X[w!?hs%%ja W~v8vvIM0IxcGL޼ڽ"EAjpyF6=6Vi9 wB ,ҙ[$ X4E}_M׉J{% *a?f0=?-UY:CѢ5`Y1R!-(qXQz;A-Ff5bﯼ?z.J"zcK5L FKJ =Dɪ JS‰Id: Ƿ_ef_{|CDaJio%X*lQG&H|RPr@ ]\n3$_U܇cCl-tHJsOg?+oXN%e_þh>#C7B:}}" Ļeu_lnѰE ed`ʷAr0Q]ìU)>7gk6z>3e9 ;:<NW3d8fb%p@EpEDb{)AMoY49K(\hg`-;3}=إ!kˎz$HjDF Q._o^^)R 4 ńq9Pe }UFEH&WЯ;N~ q`D@lTi.!BsMN. 6Dvužo':obpP.iy$ ;wLkyNz_)T>_ ڎo8/S'`Փ_\<^4i,p:5$Dom+IPk9wm+ϐOWpnc.s"Y'rICpVzd2CUo ̣7 CjF[⌤!.%ϰ#nئ ξZS/>ہ!'A:r/<56""(P"Grn f`jq9Īj=Y՘F}[a>(4omj猵(z^Aޱ_m\JK!0Ja@'SZP P m*)LW8>zb+UpC\`Ae_<Àÿ*B6ތbTgQk9]ێ0302.Y1h݊udB<~NP#J+ׯTT$*d!Oh0_@њѺd"~b]9:]i<-FҲMAfOiZ9)@S_Q-RaTͤxÕSeLtO](-Ve(OIW5$v񧃋a*~rià'Rhx8̬Fr(uK#ɳXP@E\:̶繰0o_66?4H*^-߾yG( o:࢔H&Q]`np$3#C0twhzmpr{܏s]IHOܔkg+hu/ åR}95AWI23ws"l튔*wl"MIFF:?*5MX0o.!r?Y/|X{dZ7f3S}"*g͖R3B|e#"d,Lb8ɇP /.mdFl$1ekcw9y?+]m+* L}ШےX;_ m 5q5,|yPS<7Fg0CO#V3Pu^bS-y᧴5nY7x`p[ĽﳠDw){3T*wdCb! !^W~~3\Ǖ\"w.u~ȣoἑkH|dGSvU)`cEz.TI&uD0KB4 AMv"yy I=?gJevۖEVեO:c\M8M4'B {ڀ=. *3ۖ}$[\G0%b:vۙ54憹[ s9 [ЖQ&;+9{p-X.wR}鍚,oHhZEuQ7i+w(@[p$$=p}(8UPfu e2r|ay;CY.qBL)CJ.gVw30~ns = h}LByKo^w80[Z;G`𺔌 90<ы#h.jveI9(M "Y\)(8(#2r*޶<, bJ h*UD)2C{ ?ͤ0d;0cA$ 4/T\mݑrVS.q +fl'mtY^O*kIjE jCi=Hj_k+RFA<]32 PN(&|$dnrZ2a Z {l$k*\l&ӼZ[.hv#M2 ^F3,b=fgfLd cKm6(B#i'IB n"@`ggC__~wp-S7Y'jba bVr8ߺ\c:LHO v?ƎY.KG 'S@vT0Ǹ8S0oj)=E%6!c v#i#xyU\:UqᲒ#/ 0. uӯ5 vHI<&!I m+QB0**-Cl$^0WO 9oj?p!uHkms !aO, Փ<'cE$q)w?e)i)uIOClԢ EA.qiƑEG4v7F?GYK(Rjv|VCtJӶ%ou)3'廙t?35s3U@zY7-G Z[e!_kḃ7V̧_#.uM ?*)8ߨ<}Y9fWeHGryՍ\VmGJĀ!օZ2e˱~,NpZ==G6')b^_"mxpDb$plߛ̉VE]c"{48{X،GdT뫚pKĻ=+-(,J(u^0z2,N3?eÿKv1>o Sey2:qp={9BB:Eç 4#-.e:CEC`vK̋КAQ];N7j|VU"yH4?U71?KȱI|pqrkV; Z"_e.E-.~qw`Y -nsvy"DRoXhFt"+0܅d$9ү#Y]+=]Ot -= CJprD!鏩>IaLH1Y4O kn/'dp@>fHTmj;ɵ./n|s!򂉯因mզ"֊5 -%NHzLk%n,KqR.8cBg FܹEi;9Ap|%ƹ½nj\_K'9GG$ h'aqe 駉R5š,9n@-8lURdM&8;ܽI-2P}"z.4 3u2W3齶$myq^W?Y^\M[\5?34B>?8V"{JUޠwySܠ"߳U:N%CX1(VX`əv<6o CСG Ϳ.x"+fx / sN`i;5S~P5/!!N%:l넟_8|n:*/r9(~832eo#km^EV wL XztMo Q!"O.`(5SX8j|7$tB=Fq k+WeP&\1s* zs߰R 6wYt6TRf:Hɟxm$XtI%^/顸n=XSZ #uؒ)w< ab lJHkIaL|."^kN(ԟs hc7i_0w ѲѸE^ߺx:/893h^_+rZ G!#[D#ӾHmRLZ{yT': KlGovI3M69}a Z^(7^B Jw~s6؋1{cόy%Xk 8qLc2BwG*kRńRqAN6J52?akWR+{9n *xgpCjmBSwuc}N޹z'EH슿?Ý[$W6SC^6J05-fjLհ^ g'tğPgec) F-w-fG0=[tB՚kFpNOlݩAn@f@ٌ))P O}lY\w(JIQRf%Oң[PL$] )0q:jo2lmF-}cHSÊ AMKRJm:ɀvda,LPȐ5V-qqMI9[iS[moݸOVܢ9GZduB ZOo׀ο$vY#g uGu TtfO%)3mĪ\̭CqK^/n쟑4G,d3`߿ejF֕&̭amX/ ¥J1x=}"4swsʺ]<]%ϡw-`egh8Uqcz8r77tZW$#~=x&pRv K|mh,L,{xA=6o Z3\#Vy[k]DTZNBy%4&awNlⷸՍ+UN4iGoN լ1uwl0h7֯Q[5X}PةϣAs85;6_YE# =Q n?<^J\y5 ʻd#T}"r3 ˛ 6$gz2s},]Jbj!rΆz).bҊ!Ae:fDQ4Ɵ`͙J>zs̪Vjz9.ۣ9&ɘHC6 ъseU fWa:!r" hAع!NxRG. &u$ jg GrSk=5?I;UcfU%zr(8vºc䒆z:ެWp!{!`GpĬV}!n95AzqˎDEB5-,=} z+u'Un5)HN@05't0u]\ F~qw~GpQ2{5Ռ¶YY.wSR*NP3b/kߗIczLYK,'3(c-h;[qD;2nB>s4@&g`Tb!OC MN@f00] EgvKś~$OYIQɹP] Ԑn*+QȓeSVXP_osXcM!^18Rx} .-ʵбE & rAD5SuX3E\!G&7uw8C^&,Ů+/\;Wtl%S/@_ZX򲯇m#RЕƹ0WleބQn諸 1lqO[ɳ|fd:3G5 uPIۆ@:lm $ﴈ_ڣ]#O='tw2;X`Oc'>mLյ y=U1^F3/ i[Npuy Y&l*a(RBᆸ};M} CdPiǿ)6sBnI1Za\@n5Su&S~?ΪXM1iz-w?J,J(LoH޸XqAQvAco$~ d-;D0B VzCQ@XV>M290Xp?wu9våI(Gb> X[}֮#*GNWX;V\m8:zW>LS }tccbRG#ZK cu+ƯJrS!L/jaR, eJCaBeUr;aT${FƼilD W8lByʋW+d_E;7ٓBVgR_tS]@!.DiK9րtr2ERtC%| l?8/yU{C*]7}/ևrsdEzGu_҃Gc763]b9`ջ4P·YO^ q1.Qqό .l,~)d+ϥHJf3C@jSAK Boapw~:M(e3tsX;h]{|S`b "y2teabCw)Y˪ ሥ _0 9;%8:SgIZTN=V=Ij=5![6n+h{إ cc@5vX 2*Y3(Hд- -G[}6#;v5VaxFn' I絮adƛBH ϳ,&˘HBW a| *)9pRAaeuAA$2acB#5 dz ,1c@Bًi'_Ɩ퇋MT+G{ЮKb̷Yؐ\ۻᲣCz d=>p#xm%Գ6lN/$|jLiB`SFya I)6 _G2Fvb،ק 5:z%/BN8ɺ!˩N16ـHuvbgtr:gudDf5;侨A7"Em[80׉Y$`K(X>qsAbL38aJ+㨼ssP2Hx LvB? H?R%uH}ef>v*P|&J}U+W/_B>\ȳ>,fh8sӕj5,I.-.bt884b֓ \?C<y&>) =IEe6gn 0\xN8ꏋf_ QCj_DZv:)]=#H#ԍ6=+rm}KL~Rw²wL=}}s]hdl#؝-u}N[Rݗ șew뻵di2P4Ѽ.~ܬ;WKED0Xh>rv {ra:̩@[̃."m3ѲR1{*ok];g|bU;gY>~݂ڦ[0uNOW]Q6d-.&g(tlH2'ݯוM;! (v%YxOҙ|GZJfO~vX) W^EMR~}`KPQ_Rp`Ld`~`J5 Udvqv,Gc6:}(,Cm`I+/ ri$2aw:!G]t/݅Fe^)Ν@ftJ7"1GiGv-7%L;"Ԋ8B-7QcYy~~T`oPN s?z?"T>aG,ͨ61g{ERU?|"S3+ ; ;7T,6=L_#v~uܧxs,뤹`)eB/JIƘ >6XwoSp;k}&PUbK޼4}d򬔉.|D*<{m/L{V?$S &:t*6.tFfVn'Uqc3{ڣ4nNBNjNґp]\*p*#I [=IrIhu5+8m'0BjC"-Xh_ 7_ KWĤzj*òz[GpbٚS%:r vU1(/Q=}|nI:Q$v/yň-j2tuúuezls @c)*kR9kԲR Aq%|%<d&Yˉ0+`%Z<9>5\]w 'kʞڟPlPm mߣ.$z,+Al_1bVGخUf _IAPS3~U!a)$\o VԐ8H$*a/V}.#Y&}\ i3q+†!@;ޣvir)gU;H<4'I˟ I_hf*7M'G6 P Ki>Rsf*l:R"y.qUGgs#EReQ`F,i+vC{݂Pc;H 1%%6Na0{ vA) p)nZǚQh7J'޸ yC&ifg'hRf OPeVUXß8dgP{GFegjrqe_U>қdF+ !տeV1)Agu@ZyfPދ!2wx[v`dU]p+$1 W/: *|dV!e; U S̐ջe2\txMӈд7$ۃs?Niw9zS)mb׌Ч 'ɦC )sb{Z*)M-s?<\9& Yە~,Ss}P"9THO1_u1X IO M?s}u\ ~ #cl?"OgAk福gAteAeގ9V4|ݍ6+GAc?K4-*oxŠL_LV d~.ڪY>\є0YNQc>U47_ B.F2YG3`.rRo8YYsO+޶h0|ϥZy_Zt>t'L(vݢ$)ƒyM`^Ӕ7 "u NZɳbCKu]<4X۸/l@Pp)1y,HxT=s/!OϞJuC]8Z|ǃt3 VNM³ Zu >ڲ)9Rn0.*U[ڏ=;Ѥ1XO*׸ ~})FsX1'S=˒b(0(eT|RIs"J OZ )D3pO@6hDi%!e@}1\/)yK^8/S;|}]hkL%kϳX3U!thCFK9 NK+:һFMjZIFq'窩|sZBi) ;*f*|GY8&;t%{R>Cm2[!щS Yg7 $`ؠےI$I$I$I$q޽u.S˺E5G9[ gqr=hEZ-6I$I$I$I%3e!ʧtx\)Kd^ٗV8%ELH$8j*rJe[S)JCNOFo+&*=7i?+@e!!wI K3Hn#i Xs{C$ 4pSp2B]/R\n/ Yu$Ԍj,JF>,dS% wzQxT+'6 #L4m{=;Aqp >b5;DO'XiZ:kug:ke}C﷮\?n~ݩފUΉCF+Y.5vLRQO=VHtkmy~i*Ќ0*uߨ3Y `C|= ~X>E$G%Wp`8 ̗.MJkZYQJwx6z [ow9Y:YB閵6U.zs.F_wnRJ&E }&Qy&W*>*f[hxqQ]{E6uJt]C/^Y$b "`&NܭhYqC>^07ZhN;|`-rna+v/+e1.VA#Ψ[G@~Wɨo5́vsuL5s 3`q/Jjea*a*AvWx:7-z#!@;\ZԞF{嬤;6H|xnB9LBE#cݢsl܅N)sQu> r $Oˇ+K{;%fX`čIKd <}"e}5t{P?W` М樾JFZKk ߳~݄bQ^dT,nGO^$tY1sX F:s9߮N9k5]MQho5UZIԈI]Qjxmums7z[ xK`~9ūmeitP0e#W mo񽐏6S`[. +AjoeŸyCuE,nWe+4N]U~,LMn#geo#ճ̀hv ^4q~,vVbgS)-e"#G^+z\Řsnȩᬰaq6H4\aM0__o$2[y(/X|yHSzs3:5, ^ OZ:x.fLY#qR,h^ClvHbĴ,:Qx_0mcT̵]B+(Y/(v0d2t %b uLo-``).=sGh1fYA =Υ7Nv-@>gMKkT&JQY_jt Wwx,q+Hmy|Y0l/:'JDž5? WA;E㰫 Ь'یtqIE?{o*_>SDm١&o]НG2eLtc=._kIPhG.PƸ]^V8Y&Y:;#_0Hp*E"U7f}Cm G8YД}TL'R,i:ܽ!f|vu.Jo8Мx]x-PU?d _̄&#"YUPhSޢ b(eSC1|%Ut9xquPnM2Cs@&K\(r<|68`-Ͼ64kH?{b i%&n,cfrcݳӎCGN-OO_:0H+7'FdiD!tMnAnmqnz.La8 CFAN, YJ$%>aw&lQqz<@IK7f5 JzrT:`\ʹxӐ/0=tVuČ ֲ5l)jؤ)B8^x rvN AshNzp CE\7B/Ӈ6rF9+ĶS?oV۴8"ƴE:i_|z F;[L*Ibind("k^9).Q*TSΕ<>.B}q0LI*! NZ ~R[ pE[~څE6[ :d$CoY ; ɹbJSLD"b~oش-V眺nP;i+,Ŷ|[J7+}_3x+ӰZ(yگ&t˖T$3}ٽB3Qp=g^π35&pۘSÿYE{!Q .% 6/) ?8W*/غO;kz%Bvq!`nx.>~9l].-+҃`N8k-Ꜭr'_兔>aq*5_3Ck1; d (k.D 3{AJTkwsO1v.RΝðb"X5 {u&!^~u0Fo6V*9ܴ'گR-.ư$GQ)[:*ۢ\_6vMEaiUfZAx* $٘Vz*?m֫]HqIgm OTig;/ЬdрGܐSimu,.`.qvT )UIf:QNxM qA0(Qd#Յl6+ꗵ֯ȕ=ÿNg0LP%/S=RڠXH59 *A:P,cf)-[\d7:#ZF5X (A̠CRO|JCWq _Uk*#3YNdTD!?7"YaT"[xn,5_ oҊ-0V-R-&Y!Pb*}ġ졲v}m߰<;]Vb>ì4u{0b&煕xg=7MoOr;UX#raFzX6]{=\y>&̺!DeZΑ.#5 ,+$A'D8h멌g; .ꑶh9 9s;#gTl}}r᪉^3]Ři&=%Qd<ث)PhH@.D IdF!:<.Ź!V{A5IvKn:\+ VMX8{-.8uR,+&1pJDwV9|jgh趱VCuw+3XzV.@|3)5f+sЧ0ĠVF)i6dr^.ք_sINsO!4AG|Xrr:2QK (oS4Y`'T by~)Oc;׈Ql&vA nYںYb\{A?/+e15J*9`'f%qv c=LȽh QojwaMFV7n[a+%}0ysJ ' 7ֱ2K%e>mAKx} & 5B f4:챉-FwajSxA- (筝 xrl_L 8o+綕 qgս UI(37?^./T.u;)G֢y Pɲ"Nc9)fzԺUAH3?-fGțXw\jl*Fyߊݰ~Yjw[c$bȱ_4Yx :38Qz6 y9 -jTD~z0`CʋތO8Xr|֛qB#6gA\<&.篿> Hn Lן1iܛ=6X;8bmE>A ߆ч̇x-{3#vu#ذxq~C 0 FsfsٯC 6_Pkm4lW&&J[4Kck撽|zMdvKVP!VmQ ڏ 5$>gA{JVWal𪨈x>TyV(?VrĶ #۲wdktI [!+0*1YsTb'+I(`D:$+GjSe2Fybɕst\/S,| @w/gz]#Ƕ z ZK ,\$X…^;֦Y:1I;;f'kJ^5Lif\%=&`&M7e*7$gӏ`/Śux>X4vpdG?Jx{c)[*_{*4!Z%HO+DW}Q[9}E_oUï+۸xoF`G# l$!JW/dG`N* ~[5,\Qd0J֢&υt}x#==E.+^b $e/fqlܴo:g&xya쭯&#'ttPJD.$sCQ Õ1PVt=Ы͚{.a@]9[ܝt.+]X>QPVU8$a4)2y̖9ٗ2>yǨ G_oh@%$(3K6X_'oʆuݢ%]?łJje_iyFgb*S{yRNJ|1퓳ʻ}q uNjm2۠|j9vJVa㍤8^4B3F jFa@"ZachI@,Fq*m!!83jUU-%ھnYA'ab} vۦ+l yPkpt8๬W< >itLCGjrgQσ~x Vzd`$F<#JM? ",}<:LbvoDv2&ȟfͨ!z/fZn8Dwrg&m4otNC QE}~֤{des dvŗԁ$h͖F%)Ǥc.et7?~PO1\+eèXB94cSQ>X/{rIs(2B!WB Sbu^NbA>GO- T5l3J!:?Z,ݦ]zQ8i.)s$}O)i0*]C,=RaK3O%C{XЬ\*nWDSaSI3Y5-t ɣ>DQSo- 9ܛdx5Ӷ|pHĜUF;3m]/nFf쪰:_؟I9 j<1?Fg/m8 :tK$/8ﬤ٨ ЇNS~XE(\e`k_ ;|;B.LQ w[&f}2u}NT}KdHM3`Q:*nrꈡam<6IuZ 3 N6k3L*UKNӐs״݊NjUץ1IzM1Z~ YdҏpP ^}_ W͌C3#ZO+qGizH<%/Ř|+5(oꡙ`~om2HˈLo@{/Mj^EPWZ5Q^JLXe!8ʆym em~ m0ɯaA̓G`Lʀ@$_,G:|Y c_`@0LXP!9{V6g9KNAh)wWqk1iյe3JN5 ;dkX"(}oNc;g~~?lj*@`S'HS RY3~c)u;jIB=Poc?Wv(eXt՗hMЅ]QT%5N֐Y `oGVJPy- 1M! |搵13MϹ*dP*WnJRWXִ-D,ť6Ͳr\V\/6 W3E0 `h+W^`%A)bx)\ [b@d"JP*toBC~R?IC,iJYekΈ5+ |=8NG/wVa R,ɫk@Wx,MSC^žWOlVIyt5V3~B7tфV*u&!^~u0Fo6V*9ܴ'گR-ٸjǰ]b|uzM4)ߞZqZӧ ϤQ"7U׼ױP$ r^3Ei/iOmP#EL$&DvрGܐSimu,.`.qvlkiY_' 1j$cfq2R6kmG-aMd[$sHӑx<),u%Sђ$?3*@缕8(DVEq >ۊ;gi&Ფl8ycKsr w^LxV9 ^v.:gg@ցFų D>nW@0V|zo*ZrHsorzOa|/Yq/<9S&g<.$;\jܧPk߫@):̞&K|zsbsDaQ7%ᔘ!@S0'W󳡸|n/W*C;FJJ'M;~-77`sP%.S7!b _@̀[ϰJ?s.-^ݪ˿n^ o]~^QG9(ԴQ YYWs O+UEf7yܴVac1ʷiMw }'Yvq~xWh_TPd!04/;pUm2<(EIDDף(1[;\9& 4'JAPjU)TC $%a #; Եb5f[une?Tp$,Wjga\m-q=pÙmL2/`Ija(Y:%m%O]^>P,cg{]) ]cR'UfIiKRӼj4}Z'' ozj΄O9^Zƍr_~Tܧc|4LŰ˷Ec.Wtlx5%[gы0Sowl{xMgm8Ir.(f7)e[C_Y[Ij$T]"p51jF| L97wiS̐٥[=ĭ2m 檱5GЛ uGK/VaM^gqS:(! ~% F?#X0l?, xKV}лF܏f olqt CW".GDzq!/\KS^ro`)Wm{~@Mrn9&zbq S%ei, g|_KpڙV7k^0YT0)NV)`Ǐ7.e2;{NpbRȂSje!Fw{&ҜȉzK(S}9 39V7TZz}Q ?R?yw-o,jDoaJH?g ƥzrpJ߭bt#<ao`7O6Ѡ]e NM? J׸6n%v⨁,f \m2(l#9[A?l΅:+i[!Xd`F0ɧZ;厩e7~xA{E-؊ k丱Dҝ8lw \K᝜\mt+4|$u-1w7]&H'y)qG[g@-xپn_X@ bQp@rEAҟഞq BL.C xuf#:#G] 6(nxYWsAZ o !&yLMDK ܂ ,?ƅ\y0_AV'5d;Ҷg>](ws) S]l3#pM5>`,EP2R0"E:?߆4[b訿'u)B?măY4oH`R[˳ouH`>aW)QTk?jGoo%-$C*8lw zohWW\"\6r.mJ\%,tM` : ZnƘL]cDF[T(1Xdq%F͠o6*#*zQ܇o ×MLLIɄ`'f{>TуEji6+E"8.jEkf{ >IGz3SETGPT +l E|m(Isyb qզr5Sh)`F{/c)䔟[eTHPfx#ZsoRH|ߐfƼ`wbt2涁_JI7*̓ ZgL]hxب&xt"s:Hks_A6[D;OC)>̙te8n ;$U>/FjtuItR"+B~Hш\s8 '[B# !1Adi|J\OŔxt+Tm<{5T$CI0 ^2p=*͜l ʇhTH(S۶TFmKVA '*Ev,A0u!{۩,bVCu+jg fCa#kAm5awxu tEIYw9I/АB VhD '1=X잹5ua 1dvo~|}ka*f;I%e>mAKx}3us_n:*gMmI5Ԇ,$obFjjqdXw(cXqDZ:w<ywm@CfnxNNU?HcI$q!m ]/u.9BLx'SC0/'K3dG@n֋* 0` =#̨LPNf\s2.TrfvޮQЍ{pnj\hKkak"I2EW'CۖKD)w\a=cFf>B\eDc:sa3 q(k"1OqX(ŭDRƮVRadKvW7qRO[MS j"seR{G5 > g }]]DUHؒUeh4㛸' & ΊʭqJ^A8o'f?Eňs퀬S R' jD䌛yro9 dNm-P(KKyDž/2y焖2_h1x[?1"M:i@俪W1}q;@N }ԈGTO xvޮsgاuO]y}:7 h9TLVRM"&˺b>¹PzYzRq쀨(9wyz^[PT!dUJ┍9;M:} x+5{dZ%j3.56n’/zV <o}on`/;_]&kp 6¤w֌ wfT͈jUcq@s -2\EyݠܙAntߟX2裖z[erfiFšQ(Ѷ BJUv⿀"c` V{7\qdX#][&d 'xM: -a]8@TYa}0ך`>ـ!Y kI}?E+CGWiJ9%:)7}J6A7?> HKqFgހ3a[dxsFmIB;R. ,qx + vْQ 6VyӚfшk/5ݞlwX0MFT4z9]H 7gf bװhܡBVPoqg$Vi?{e)A宕m^*X8L; ,vAeWhW/| YӷVWB2"Pr}$^r 5ZkX;8֕AV0&#.q{l 9K+9ñ.Bg(p/ F:%wT?ٙ^,uhZAl3deyEK 큯ݟF2 L&F^d"hbvBX&Hd~fb~)Xg'+ a$໇ UR" 5 9iT }k NX\w3G2}y'<?:n8`EnQOz`DޚzA7~ިoO~ݕoz `G# l$!JW/dG`NQzO3Z|7tEI5dd_@x )7jhu R;nwzKFk]\40wL*ry+pFӝQHSÑ%l3 -dm[] ڼ yߗRd%H/dida 7?j#yc!/s2C/Φ_?v#2GۜH!!_[o<~>C+gPPM@ )oDy(h~ٺ759g|(2P҄!&:պ\^^8o]^m7Z@#V$;e_T߀:@"`Ik!<}K>d\>*\Mř%'~Ieb?@p~4ePXCAUE{OT햼1zH=٢ﰃ')Ad$A%%̍5Eܪ3E2.OUDQܗ0XM!ץN@Ԭ;XAvE+@/2"XW'_SQ(6o J5 d ?89H`,c)6f&TCU{*w=P gm-I?e8Ж~lhaK*fcĭ'M}/$wׇa++M:R겻u共I %!Sī՜8ǶM:yJ s|/ݯ9 g\~&8\LV5. wyĦ1o8xCȎRY/E+@ ۾~Ÿ_VCՊ0RB4P`+&ԯquoWQS2OBi {xSB"|gaR `'i] .q|jE>QnIEUcm@d5"S0i'4eUym#AvdWu9픡'gYKoңF-~r~\BWpk4e7,n(qCX&-R+x4q8Bdm?F::/})@;th/9;O=A# ʁڋX'Py?^tdDl٩_ÛC"BjC[Q8=pigSА,R*W:qݷaG㧞}tz~?kUӾd-H֛v)554 ˔ewb92tF\/_Ki '32Z[RLKR&VdlZ|viU0fCFr7kcv=P{mWُX`Gx2w|r)OaET$`mwI>s72S^Ppf aF*AO'xFRa5E[DjUЪ_@Davf沒7E(X# *%! /Gߡ)͛BɰT{le$&:\cTn=<8" Z7c(LzA+Ꞟ5cA6s˕Šuh>XL^9,<~Ao.۰)502b먈hZK7ɒQ}J_ޤa4CGLHps[Y--9Yn$?e#ZQ`JWzF5gX*qWV._;+3 )H/ +*;l;H/Zc3󧮃n e?Ŷ|gY2cu;"$߮XsMU/XdRd7B_^xfj:j[~`(/=&SܭC"\(Z3LowlS` J1JƝ618p!+IJËhBc/{m- <1X ޯ|eF>)&? FCxd?(0B@rvFӜ\rPQť|pj \Ό/Fa:E^VyJZ PB D?Y_}ħ6fÄwGꌜ5Su8VTee%pH-`7`> cUZ**,o=JXLPА ƌ:M? 88`4CB~[yՊ\"Bz#D;>6w+,9S`Z`u-m xJs"%*Ddٗ'`aߞs1OJ0; 5nIx:he8}fJDZtvHخiftz9 uEv%/B>L&8ySA ®+/WO-=H(hUfl_agj^V,V>?B<_Kc{=~6 #1A)y8d_'8W=\ \-u"7&Pp_;}#tEMc+ΆxIZ4ANCd lw"Ǩ'z`؜ ͕ųzOBh]tpGO%e>mAKx}3uΫ}N`\8fDm%9f-Wⓔs2iWq 6) h}N"sU#aU7vsen4m}U]-lJ+Lnuc+n>䙲i lK2tx Vl&RMf≜E)4zpcQwayƀM^'5Yv/SBRTG^8QWfe&_]HǩQY + 4T{m{ \)d?߹"<$Mݫy(9/^ޮoSGW tcx5Y5]\(S_+A6[]XX5nN~vdJӯťr[yٶ\F~u/Od~p+n&]G{qLbSMn4؜@+85agH"?#O{XUƻB%&"bVI@Yhrr!=t$8K|8L3nLJ b}3i0KueZ*lEl5:vI)qf,sMX]DKe0J p%cxYg/g.TSFͪEt=T(A'4)*^#;l*JRWf\r[5P5 U L`Y&TlQJj T KTMfPS dB %A0޳Yf k?JVq}?@r+(fa;Wέ^P/"2 9mU2ؓ Z2y444>VSPU0y99e"A *Z'e.v\!p`ytjf`gb` -So*zK@#  ,>%-:=BddkBR \C YLlu5#^Z2RDDŽvuҴ 3 ȋ!03!2aoolңkP*(H%` ݝZXPTQj@+}X6X,lBCXΗAmMcd'sqK9lŏ+B`e+[%'6DB {Xz:f`v5-Cqfzx=g8e2c n`1d_(F!y3)3sd)uxc kՂGq(+_,N0_']ۯo Bhs!:/l&I3n[(J}Q>HK 6FxPwR`ġ@v ]4 A\'AޤxF ~.x㼺'Q zFUB"gUQd鬑k݌0y*/f/׹҃?ݎ[B 0-$X2i?Bxer g/Oۅ|llO WW-G]ԕWal6 9%I&u-ˆp:m (l_HT05ѣ<7d:gDH][3 i30ݜ,5,Q޴ϲhЁ!SB39nQߧT8qgQ77ujb1kUs&.,aԢңsO=3018fI_#5萏zK1_$^2J= }eD8Q6dZ>|H5Bwwtmt`m#*CI%z&!_輡+]+@EBjYx'6eyðsp :t}I85(Xw'8 ;Qx`2vOckC8t-bRdm +0cKdd6xb ܽEؔ36 † nGLJ@;*&'HɾjT,] vq Z9VP7own"X0 ذ4;cp7XQk($ƵXmޤvԆ8CގN͛.EF^ bVTIjOÎt?,pW!:om>^ (V3omJ0kˀ$tЫ\)Hj_?!`7SN3LɎ@/YS3R[ tM֋ 侧Eoi .X(V t`= M'UoowVOo7Hz ըQhUO>M UjVþCdbɊa++E\Saa^qu+~Ho?VD_ ŷΐïA~& UL?YVqDin=́@Qfi\8^I4ќ;qT L#NU%XY"V F󪅧gboI8 NfQ+Xtia<ڈg4'\p`},'r'8e\/>u*Ϙ6HQ~\(+]JiR(ʯN_B Րqa8|%@:epHij|8IJ$ O N W|iI:|m1;;˽W*|gڨ{ )4#{y l^*(@K*nRe+Oe:U+9< Y~h_'h"*m 7pV5!b ϕh%H(QŃwKN\BfT'*zyҫ™QtٹGZ -CV\\'9I߆6 nZ̦@.:>zlGǘqE.t$4y'Se(~f0taDM9tBl$2O@*Bj`.?]A;L>Tp sJƔvֳZ.]r^}`>02q'*S#lv #@^\h헒jW5JtNY0r1]PYH b=W $@5cX@x9lt+A6deX\gVd$f0o* {ICu|Fg*'IԷ ɐpJlpjԇC'BJ \;ގ74Fh}a_0WuvS;?'sc)">hWcC7b{yyFATnm"7F϶Q$?5u aaشT1˱6nFk"y9eZ*IJ`]+Tf5K;w vlwcPHZV W?KѽEA<@)\D+ N{dXІc0[N J/8;; Ѝ?hD5#CɌ)x0ybZ :3|>1s  jS,s9@<1㾦8,UaOνG)y||:_aZvT`<bZBj~eV>n?C/U $IN?k)RڗRcfօCLd"WM4>G< ңqiYw A`ɃޤAVx VU_=B!/ fN ds͵1‹4? k[.\| ]<ESŖGBeJtGj4r{~L\хPN hv1MR;Mdsx^֢35s^5ꄝn`X2HR#q;`&N|ob0L1 c03AVGBVj'lC*I- tB ^ j#8V5jY1zc55P_aNyÉN#Y|c5Tx5)@}QpOGQ0q%FCv3A %kQ ə~l×d>,Qm |8HQme2v}0GZå;!ev H܁fI8]fFi:#8*+_\Va7 jzdaӔ@ J|6, 4XKaz<ń$eHbo} "bmBΘ| >)xZamH7,f.>L9(&__A;c>{_ɖk2pZn_ڂu u7Ԕ/"E=ęYt͛@5xDvrPS8⋏@y#77O\!6̵I{zkľ9Ƞ]H9FSoooyx$ҎtVSiveoYO0>;+[X1vpE. 4"{F{@o<ۀ`%.5zrWݝYG~^狚V T3@mS2W)]9O8zfޘ5K*hwdq@ӐZog(.9b'K9t2AfDeU@6.+R0|u4fY;Y.t8o `C}S_1Oz$ ZTU'/޵}EE&H("YQ-G1j[_>EhHs]Za^"uXxUd9<+r&v^R2mzL% k>NP(*}D+Dv^-\$HҢZF[8~  u{znKC"FX{rVb{}lyI.r'hnI[21V]>YҞ28 veLHo02hgd*Ua]<& S7F;ݯ4UTXju30!uBEuJ͸˒}KKzeiJAvɒ"v&Y^9_4G\ 3we[H^S;)7.AW^dSR/U鯣LňJuю\L4lPDP瘼 ^w6 Tny M9qP^W4ƫSpD@E'a⮴nGkXdWxGE§#.=?QHϮvx聰}%c Vێ:{©=ȓ( E`s@%;Npq+J۸Xm@VymMlTb6P)F\—Nʒ #7 m-VJ]*F`tDz9=!tZ~+(ܫg\7 ῦW@? ``@%T(jx`~6Ŷ?oo7Ǔh]s S|?wyS-Hx+dM>(^ mC11>ӄ5= BtnHm=fV$F] a->H/؇MF;Pevw݉ܬt7 3@\B7O|zdnߊXmֵgϨM&ZawPQrjzw7Dsف( -i\ Ld@\( o;\ě(7V$i.G˯jav4ʥxIB;Gb ^"̧d8ZҀFգ=q T ZB)XRl'$NEJ(!Hڡ9{1뵇2V9:NG.!Ȧ"nScsQ>1g%b12"3.goU iP]T2(80,dOZqGdX}pQX/MTR۩_HgD~t9՘ {pAoJY0WpWistDueJ _NUϙ&WaE~'USh1N0ʐHN Yxϲ:$9$ $ yr'_%8{/[&(z7$% ֐J0(cIpWKkA5;!W!%" i/pm%YA2>Aɩ aUwZiԋIsXrAFQgp_ @ gHCFۆ&Q=1p+zaQcEWF<Ğ5GezٛrY.CʯGC-' 0$![V N$+a3Lm6UYw HjQ9|^Ї4 4'x{q 3;T(q~jL/ TbgvK<ƽ*eM9;_X !y8kKdhhao XݤN4ِ!&Qf6 ڲOd2:@bAs4H{#t((zMφ!pBMٌb5XI>aQC52U׺u?9VPذjj[ZWn -!{NF+mdoBX;`0~R"X ǞR[R3V0Hd:F(cJqRyV:b_cG KkhojCixɚwײ$vJ=ET#N1yFsdo0psUy75}$ YTi%bxC#PuĪUVJ_S2s &HZRBxBxU)qY_u?d }|0@PS*2WhoeNB1&HM?*2誧nq|P&LДO53GHl}5!bCzAtf;WrL!EYwvr)qw]Qw~dQ"Y3Wi2NIqK8Ј>hH5!/Dz_?̴L`5zFb}D*Gw~bAh+dfPp݉}Z տ"o'L'K@W/`g˔G|)@l7a7q*tWW9cozQ@*߀2U鵘 %0c\M7bE+/=ikfHMó-nkjNj\Ic^^ŷ_j{d|d#пEL\&]2|xONt6k%>YӼr)m*R^c f(I!k :}|JQ+Gr6]UW,Aet+C'od1Eas 򀩡b7]L":lmn?/ljOᛢTů3/-l E' (NX {T'3ȊHhyU^[ \O_p=@<ڸ ? ;XK\lvS(pi1:82 ujb1lAl9SiocWLaQVj{WQdyaJ"`,Vg/͇{4 UݰCB"ӫI:.uˉJ':5ٍ>럹<*x X3 Jme߱o,H oJd\"0CM[&VhΔcugD" eM1 RVyf(crg X;ww*iu'Ha~J,( %vx`G.V< AS];"+|kxKwr CW0_&>+] io(?2eL&eO 2SY $%.Xe_2j\0܂omoa:Br়Mzpm6_1ҜA6źjψ7 {j44liIH~) p)[dSͮޱ!+Jd[}bC[ HX p*rFIfJN_3r ry#"0aVZ2%uxculhb֘!ʿ<-|˒@.QT"֍V^ɠ$<__'u T +Th!Lpvpڗ>ګr;{lj죱pW'Gb[U\g}OC.؏daG4*]G{Zl$p>E^B&&r'b5l- +0BjGJtIIВ|T rMbc'a٣2- `M,7Y2!raQ=E&goTyWZX3Fi_aC@8bIy ͈>Ym*lJZ]uF;ڣ GsݛhK?rx!db Wva)R4ӑ-B`Yn܅p#e"  iURL`Ct|}9?>eDR3rWŶhTؠ0˙_0#L,Y,kI /0TYg:)ƋG }SR(hd+'&۹I{yR.1  HI .Ӕܒl zx%`  \;OI$C_0:OGBOykdkmq>ðν[-wFz }̔GvSM8 z]EىTֺV⚹8څZdW4gSvl;deX1,79o} kxVc酂64GsErL(pFc(΍Ҿu9 .mZcְa?{jiyXfbśuf$y =41H*4U(oܧ8 VR}Ը"6zEkx 16!2Q( jIyvbKmń6WHߗƣHxFc:Œ{wsO' dwhe Uj8G/;4n@`[ߓ^캪x }}0 B+q2V&A{1o=we/&YAlz"@f|]a!l# k'W,Z+f<4\Ȉⲫ0m2N`jQ6[y,hTfLICiupۇ#(i )CͪלuOص2Kyl$'Ocֆ3]a?2>Tk%pFXn~ӔDMb1g)XÞ1J`8ʕO`7 tme Iպ@'ͼgvWa6%z0H#tBP0Df(,!%#ox(4-҉ 9=J򿆩"B vM}E})D,3EU$!V(~<W'Y Z\ 3V.%Jpt/}d$oJz9jCըXiV3L/o@1i SzD#íg'\E|+10'Be$% j&Xx[縼P$U'v$-ή ^.R0 >"/%,jXpm4v;'vp:XFHgmgLQKR:Bh;k_O X@Za u- L62S\ ,wQrMV8Gpڦ͠}Va{vT]\eu Lz6ݻ8]S`gW "qɲ'ڀxdʣ϶YVL~B(u#A?tїЪu ~:SXAk`.!,}%qi/8Ӌxh?].8"MKn[$4f 5W ϔ!h[Y_{h U)+6^H.s0j[=i; G-ςTs(3-,LV{%'F2Br:rj}QM2\md=yMP \!ԘˠcMl'\KiT;Ӥ#DOl $X1 Rj|\K2wNUoVʑ6#YN(DR 6"tY޳|b{ U|\!剗<݈ :˶-F6@CXټSJLkuꚩ=PЬiYk%Hر\Tc>R?ByHQvJex&hK0-n+XfVh۰t :hHW)ˎ9@5ʈ 7=WwvS9zu*}o37׳ '@@@U6[?") ^^콇JlHzy؆ qMOQifpoB&[4'8ӰDzTͨ :v؃˂<* 9+I-'[Su-*@T.LJxw1NЏϯݥ59ˎ ^hsjP]WKK1=%hworx M]fjnZ_Om.m=¦|c\KZ(ه'r'ݒ~1cwq?52dqH\f쪗PIYDljd~Qh=6qߕ.bI͜?*Pt E I`)+zpθDyѫB%Q]Y{.Lτr+R)hR@M>Ч s{SݚR?{s| bܒr߲[.T\I1Ern`ؽwF138dP4h8a1I%Rrt;A%-ŰXYzЩT ةx^Y'r>@Ώ^ͧb&3aBbC zԒl8웈:c{ˣ)^xܝ";䬺? _#!:@F4PߓYS7k7gi%Qqo|!.9W:XE>݆4aZ,V/~~+IpEu,AG bHQwJӋ!%kYQ pi!m*$6)%?-Gp}-mdP_tWn ||b_ C&ӌ"ڕ/!i)ԺN ױ:ӇQEJn¬[KĤk$6>3 G? eX}5@IH(jLI"ɨP`<\d}zT'2%ԘxI]9eC| Jsb <9ą3Uըa9S+dr#>1s;Z^+h ˍwi&xG1XD_` m Sk.;Q=c3_1*VF#}Ů3VI{\?ǖt!ݘ!3i:RWG}: ;YLk="e ]Cgwc&\3w"QMW¥"{9ts/p FM J_DߝoEz(S\s=a$I#I[g&1{VbSTV^>t]"1 g<5 }ѹ؂zN{!|”I7oWn4,gĂAƢo"ƠJ9.=8OIA/FCz GY$S${0VΰK@lU 8lS&LA3y)w(pmIflQ@h Ǵ?WgK4M5>bH pHфCy7z!m}VW꽿Vܿա[ܗջgcbWղ\_r+Joյw϶ox0WW}WПߗxVϟooZ թPzf|"қRUo~}H}PZbɊa++E\Saa^qu+~Js3ypq0˱(a9ڏҚs#.֚2(j}By'^$k&qpÇ뿜gm#4,A8(z;Lr(6wUȉX0X540y} HEWw[b/rhBtaƪ5Dz0t .zɦuvCK4bgqB4U76@6 _8ٰ*Ɣ޺su dv_>^ `6ÁSG0+6s>~_֛V7IF2/ 2d7]Z"sܐuFjm][΁IM0lHvZ?S܋%`/zԨhp Z$Wiǜ-wE8,R "4`q+V{6}_ 3 \=CkpC9kmL}K5o2fA/4};IAsXO)>I8 NfQ+Xtia<ڈg4'\q mך`'8=F5*(ݡ4vx0U%/OUb:y*L/o_&,H;W䲢cw! [Պ a,,!DOj/P|m1;;˽W*|gڨ{ )4#{y y$d2,6&JsjܤV InȚaF͖ӺtF]V&w5z܊.#G= ҀFd4i+TT|Cѳ6J<cpڂnPu3$ H~ϒ ?O͆c5;<{M"q k.BD]tգHiZL@xa='֪J-o%Z7S^.0ļ4C٭PFDa[mtHejT'*B,i 胪[Df"TQO*YS"qT4Ł!kjq:KU/Ґ\үR;b @AJtGjh J >P*`ɠ\;kb^&ʘkkfU x{2nJ/MOjU(S 9(o$Tj cOy*|FwÄ i /NȏC"Iɚ4n֓>@ݲea|s>/.(Rg"?@=h৸ 3jD͌άr,tk#v_IKE)f3.Kc7G8S`*g$n\qf0Ӓ||j1^AZgT 2!{.Qu|KԇP''A~?K]b ExNyM/TAլ&Ԉyx xllF@ԓO.wuETH^~GFE('M8߻BolJe3KZkwdܛ5A#`]yD{Wk\dCJ+]6m KK|S D*u评40ܷNNXX/ iG8inH7v{Jeѯ\\A^!Z:Yk< WADI>v8-Ӧ\͜w[h2,V1U`T2eК&aՙ4!R,z5ݽ"6̴&S mOFrOC~ eNw373c#Vx:F%E =s͸,ådѨ謃d2PŎfzt_0(2 6']7F`_ZPĉm7\/x}K@U-/os1zq/D>R6?Wm?E˛rTC1;P1"t-3J9ˡ130!"XF<)tvs2=m:D QwŰe0+5WdG'bޙd6-`<ﶬɥTg) ޓE 4JXj-&v eA@Od,(ڞzml#f-|pM" @FWglt$jLrA[`[U_=c& ;bR&K)ʞ݁?Q 7 `z,ٯO nqZD'JtGj4r{~L\хPVE"I,1:ގdI̓!>,l?E5+\K~) C WZ9sR7P3Lff|0rwRieզ6$|uU_;{6Jo`2Fη![fW]\fQ+䣫KAFYfG8.}׋uSW8_Tu?DfaUZ~"`h5oo} "bm|@v3^4Z*_ 82Ø׍B^jف{ ,%Myr}fJ煠bN_8bK~DnUϹ/Aw AT?mw?wތ̾y1B+7%LMoi&Q|c`X䃠4+ ȥG5N>J+t3C7`4-) 3x]$U#-W<&>N ɒ{kŚdxl5;W|;J ` eL;*Fxo>`Z6h_n kװχQR!g8a; KLc83/lȯ.Sr7ÀN8SzfJ$z/d<`(#9B וcXs襮j4NȘP/Juю\L4lPDP瘼 ^w6 Tny M9qP^W GA_v'b%honn)k{:3JSFQ#ʈN<e47g.^a|+1F'YF yfp1PP2_I0HspjYxIS}Mh$% ,_~ (%fh7E&=)i1k ;rMyt[@26ۗu[m?hIi2Ӝc5L (e۲9EvNǏ#NsAtT#֟}*/$Wy{᐀ 7 [Àt,N@En.Uǡ g"9p+@o,=eP(bd=2NVvp7 ˘zDpJ'KmZ?f##7ʨ4h[nv-pM+@({aG Ѓǖʭ Kl/?N~ hi"-+;L]>/⾇A`)gHiPV Okճn˹E/teoe+G@U%J=q0hmpuF%s4*WXϱxYcy69m3Ṃ-pv`'ߎr-ճGM #+5Nh9UxI^7 jW a~371UNV?`@*by4'lei4u\n[YP8ŜVuXY>)O\rBa3pIp+lRP(6E0#>Eq\|X0H?+͗$L9JD +e0sC1|!B44R@/e$NPKu7v[ ])ESw8h޳ksw, Esܩh)BF❅ф/`c_a8.++h ; RXC S9& 2Rn<kyķƻBlsl[~2vIs8(jA1,) \z nʸgHN Nv h")/||MH)ײS 0p}1DCNKL@̰@cmW!j 7Ԁz!svx tf4y܌:1ʝ/E<l`lIB)97K $%[lϠa#SSC=.Laj˞棄`B:m" :AL6ٮӗVUqw9XUAOa-]9tSd+r">FzM@L٪Z9ܺ _ 4\L{"Y6kxfꀯ!,f~ॖȨ Pj(cuƷ'HM3*XzRH9O'YR[g#Q#(LAi 0> :?vv7^nCu8mMm&`Cz9RϏBG]69+;UX^v-/篶Ȅ8}a%u穕/*,Xs7V/`wpd@Yzt0]{ʬ2r5g%#5ӊ.α;,W< `? U'SgTMpkWQ}Iz3~)$g\Rz_W[嗇~'K .^O*My]&Tjbne UUrH,0qG>\k" J9jl.b\dI~Oѹ-l_9Uz$ bweYe.RӢWH-}׾U%cB7 1@ʾOSHԏ*>`юh學h[lND '2Qtg6Ho@@*9nOUy[L J~t Ъ%}$܋ ʈn)?mR0f3Zh6vcCET9Re+zRX9_8LՏ3ȀAxcVpMyUgmRr.U1*xu)?0TVDec75Y4bq:>0p}B0i6JaUF0ʅ Łki^He%: {g&۷9ZerNs}@EIn;_J"@+>fG1-Jms"^[Nc1~ck!C7vm2[V-3*0IHQJmfU_t%ȞPDVF; CE,4`{բ3~*quIҺs ${X?Di_ ܀ܻ#MYvnϕ\lRs#D~VǬfпwmචF᱂^Dx0ix*߇HR̳H4=9`j >M_ Ohfm k$]8ٚz?(! ,M`;g;blxRF;UdQŁV` }5k DN@gM9G'KpNGRWq<-M1cMd+Yɑ9n]Z5쯺'$HUGx&l(AbtbUў#D(RC ,4G2K}:ݥ9<"X"8Y\wQ rs` t&WHF $%`̲!Ϥ6mDQ$uwOkҾ W{^\PИ;0Mu7e~ n'HI="& d[t|f%1_zy/{dZ1(.X}Ada0$v_<1u9,d;yw/ -ɭp)*Lq.g;UKl_C2PsP|w}N:<88.$]!(%I ؓij~trswH%;$Zcoi1ܜ5ML7"u?t8D,kz0CĘo38~3'z(W6P.yi %*NMuT,Wo-[<)DSHP%ȋNDi  鴦h:z1 6%-̀Kv|p\J sճϨ-eS%pn4h\Z3~kO'dzn幵_nyrlV6' /_% R˒mC3wGk) j ПLjAF8jf_ҫ*ͅCm| $:.ĵЎr1U{8+A0կ4K G5j7Έ"wQ00=eڿkk3;BL2n'ŢPjO]"<ÑT(/ Q{(9^.+Gr !QJS2!/I)N{y|g*FƤ@Ñ3ljOᛢTů20-|"Ԅ',JPB,kPVΔ6v#,|8NpA3~<$w_)N&{SE&Jk1;*6@fpyJ` V(th?;sic[IYU|*4%Ԡ!j] M-OC:-Ne9\\?MBmR=Uς,{4nI}&'>ʷb6M~artX \^ {z;2T_ W: 2':y'${I TTE#G^P4𨼾0Bʿ0%{GϼY'{b G6cVtCzI $Qe*/p{~(;V36K^$0MF󵤅D^NeO 2SY $%/(;l]0C? K[\Ova?Oy]|P&Lg"TrF7ѺS&طMYA!S/` \vx#4|E6stK/,4)ˆ˗ߥƿaI)\x"H޿ ccy'[A{N)ѣ^]m;gS#W"KR; 2DgMY3 X2 4ٞFMq [k RL"=>8G05z;P2F=?km&L@'˜E9*}\7լVo+ OC.؏daG4hxNΰ9 \uiH9O&ߛ>BWrdT "@P ,4 aլ-usWb3Nqm;pzYF8%W<)@#~y(QDa|D̡a+1e<ۯ]ŭ%dhnqјo[ xh#5k&gHң  ^7?]@w`G2!0`}oHO`0žw?Pkl 11ZzZM=HK ѡy߲`\43Y؎m)'HM(3ǬU ә8H|jRyOյ6dFrH߅9XÍBF@s IȺb0VW&XaVb$H AX͜P59iᴷ/YHyFAŘNݪ[2.n,Řʬ}~FBW=,Ffߎۚbw[B߯ҿԾԎx3jͿ ;%"OhI W6Ij=V%=dRoUwnUڪ67WE=)k8`K'dtA`N(j`?Xp->vs gJ% l $Xx d8Oà__uڂh7X_^ʂξp/i.3rRCJ߹3֞ADn[J` >(f%z 9#p jȽ Ԡ? 0A6+L [ 9nWL/WceoG$M^fh 4  MyւUԢ2#JuM)M"۰s@DsN.~Q h֥%UyLV~~(Ý<Tdž=@Ь%v3%eIA&0GEr2v鶛C10 qMOQifo78ƍn&r;xBeZb]LFҥp}@rJi-/3A]J*/R hV7UĵvȑX_„z.Rx 7=*{I" oS &T`P5sޠyЭJh)xׄʛzu.Aݒ~1cw}GUL<-qaquRu5e/g×尗#IVGo'B&LMufOtʻw [h)VBO)N#Ge3uk.++;XDyQl.h<{^Ɔ*k0u4*]K2tIP2pH->͛ؖq`;:$RQD>)o4fT6Y0V!nF5yҭ+[ z qS~Q3 kP&NT01~7ċ^>vIQsIي$>T)>âhWhی \TArI( Tle+Da-Ge1A=K?LCAnjbL}ue('qs]CY_ gy/Elxe2Y<m kۨg_cOmf.T!2V#@qfh39☠h.#`4n*'EU$_kXRw_ ̌;/N0p^#ڙ@TN v gJqw? jJjD 9 0Tj=Z&1Y@H$s\ ,fJS4A0K۪Am}^ؘR# H?]smM%QAU ̌DߝoEz(S\s=a$I#I[g&1{VbSTV^>t]"1 g<5 }ѵ9='=c T=1P,ۚmml$(Da4p! CQH\zIop:I?$ˏ, o|&P.$b$aXGdݧXT61U[P!n0l05js/8BxF~噞`OJAk*Dk]yo2(٨ ӊEvo;Wјn#S_ ųޠ?P'˞k-@&PÐ.67z!m#\?Vhu|>V~ZWV~&Ⱦ{*6_U?tVOsoZ թPzZ|k_>ߪ%Ao~l;;րbɊa++E\Saa^qu+~Js3ypq0˱(a9ڏҚs#.֚2(j}By'^$k&qpÇ뿜gm#4,A8(z;Lr(6wUȉX0X540y} HEWw[b/rhBtaƪ5Dz0t .zɦuvCK4bgqB4U76@6 _8ٰ*Ɣ޺su dv_>^ `6ÁSG0+6s>~_֛V7IF2/ 2d7]Z"sܐuFjm][΁IM0lHvZ?S܋%`/zԨhp Z$Wiǜ-wE8,R "4`q+V{6}_ 3 \=CkpC9kmL}K5o2fA/4};IAsXO)>I8 NfQ+Xtia<ڈg4'\q mך`'8=F5*(ݡ4vx0U%/OUb:y*L/o_&,H;W䲢cw! [Պ a,,!DOj/P|m1;;˽W*|gڨ{ )4#{y y$d2,6&JsjܤV InȚaF͖ӺtF]V&w5z܊.#G= ҀFd4i+TT|Cѳ6J<cpڂnPu3$ H~ϒ ?O͆c5;<{M"q k.BD]tգHiZL@xa='֪J-o%Z7S^.0ļ4C٭PFDa[mtHejT'*B,i 胪[Df"TQO*YS"qT4Ł!kjq:KU/Ґ\үR;b @AJtGjh J >P*`ɠ\;k@S&@ 'Y&ᤫ4&`ă%F}i+YmD Xc 0"gg|8J@/pm0ޓ21ZG~F3Ѳ">9w W|wZxΡRH=&ڷa4=`"yV-LQka NүL(J*dh@뤗JLjk=stٛzwhU9q?Ϣuݜ0f Kq!%3 @,&hnY-]Fk6wU#`~٦*-0ZwÞ%W\MlGVH3q;w$]9o qTWh1N"?pi`AnܡuFIFzݨ61D |۽6+ yY^CXё3#2':y_0(2 pf____M-*2jn+e*/ϪGJl߇ .aeP'e.nwY9'H0B;gx!Q8DK!ۅy?$~@eJ=5u Oo#O6A}dwCFEGiXh܃$\vG+}j7ME_-gtIhcYtrU`qBkE8eiNҎI|yz#ٗBIc³U?ЪФpn9478nQu3'TAZc/3 `x"Ows:7wV햣Hcu////|zJ7sHKv{3%?+8S+TV926YtNFGORSWvZABq̳R!~c̥B@loA?,yI*.yS4c8łFXQD頑BEF ;sˆJK[|R=<7ʎسx>}ekᰀ3s Tը2.R?@奿 K+{ᰅ$H^dRY87Qf8VVBA;#"\V[B3q4 Q8_.,7'lZR`4RSrxdkY<䁩PA>!o%(E6UUK *" aA`QLwZ,+ߕ:hكL+#f5b4  kc>h XVscJuю\L4lPDP瘼 ^w6 Tny M9qP^W GA_v'b%hon m)k{gjܿ˻7Y KِH7g.^a~hj,:Z((VCZOTWe*a}>aS7Uvo8_OR̅!HTi#-8m0dg_ Miw`Э&cJ,(B6kUSț]( Oyx}:O$m6x#dOn#Z|QHj+B}1EK@E9j[%"n8c%J ~KH-6q;o,=eP,oS ΢8ivJ<#5 +lĶ䇃.8̕CU"FKvN-}0]"X$D<̜K0Nu^su-A|ƩKk1n.+RcғSnHB7hW@z@1 zo_Flqk^/ߒA!Q,U),_5X Z/F)Dn'?Z V0"#-Pܠ ` Qg7c-ЁDNn \͒e`n%IG'RLk2g;)E{Y %oCA]aݕ4-O9Jő+~d{oE (rDKh:bp7xRFr,sг:W$>9F}ψ+ I0f091a0*AбHt5 `}qPR^+cd"\!ﷴFfa3YB{O^AL[ݡ-H9}DAWT)'cS<侈K+O3P:|@qP/(16(TT/(Z7ΰDDsehw9՘ {pAoJY0W>D&1)N1SU9m}(ij:Bҩo,}ҮZ u e&-r A"H/'x;o@q2꟱w©{f~Q-q%a OhwZx*$=hYijdHuތV< Xߐ]0ă F5Ng6aT>X0H?+͗$L9JD +e0sC1|!B44R@/e$NPKu7v[ ])ESw8h޳ksw, Esܩh)BF❅ф/`c_a8.++h ; RXC S9& 2Rn<kyķƻBlsl[~2vIs8(jA1,) \z nʸgHN Nv h")/||MH)ײS 0p}1DCNKL@̰@cmW!j 7Ԁz!svx tf4y܌:1ʝ/E<l`lIB)97K $%[lϠa#SSC=.Laj˞棄`B:m" :AL6ٮӗVUqw9XUAOa-]9tSd+r">FzM@L٪Z9ܺ _ 4\L{"Y6kxfꀯ!,f~ॖȨ Pj(cuƷ'HM3*XzRH9O'YR[g#Q#(LAi 0> :?vv7^nCu8mMm&`Cz9RϏBG]69+;UX^v-/篶Ȅ8}a%u穕/*,Xs7V/`wpd@Yzt0]{ʬ2r5g%#5ӊ.α;,W< `? U'SgTMpkWQ}Iz3~)$g\Rz_W[嗇~'K .^O*My]&Tjbne UUrH,0qG>\k" J9jl.b\dI~Oѹ-l_B#UϨoEn1K^Pׁsz|]o% Sҷ[7/r\KLŻro~2W`y^h9p<|zuώ$X\xI]D&Hʹ\ 1l2\mo^W3q6C~$rѩOxݔ- pO)OV|Hag' }o>zoRDd`NV4v7QAu"X6(\h)L腿OlcLA^S9rK=1@=C\ms"«z_1jYCȨw@^*V}&XA&r>ÄUuz7;J4 >30S*ә̥#;?+Te餁 L[(Z؏|Q):52}6;9@\SgɟzrfZF;`Z(!@@md n%KmB4`x Gs63$F ~:!y lec(Kܻ.ͺJai=yO:6|0n^:1kQ15%}⺀%oEyL l5v*|E+F~s)L].aw_H,}Lnl.yX&~T:qbRVMH~DE~$/l,v$Wxd y)@y#Gmӌ'] " |'3:?6?~.9PФ돍t;fjVʕfeރ{q<2(xOdߨwVw {cE;3SK A: CM#O~906}n''d%S8P/ќߗhG[[c3s=TCcA\]o 79e;WlzA \ϴ*0_R+[s80N NKׯ5"z+n_8{!xU-L$ jt=)8&2CNhEN;K_'61-ޙhob (^b!-6 h)Pa˴~ZUXU k0+I68ar ॽAHQc(C)v\|-a"b8LY )񍓹 q͆xL8&oz-bX1 ʇqÐ _{%}GFDb͝v!x9 ͅ֐ UYj|iAI=ef̲0jZХ^B,9Hب' A֓!V"mH)d.L ؋9ss[+Q "8~3'z(W6P.yi %*NMuT,Wo-[<)DSHP%ȋNDi  鴦h:z1 6%-̀Kv|p\J sՐzyN .✦+p9?,Km; W~I<ܷ6/=p]RNL$uaӟi̓Η^V]gƵ? b  gg:iyodDWP(y˓ka1YЀe dHR3y_}n6`PYj-zCHhlM-`}{Co|KC4#qKk=lPD\b/& YJ^NjϚ|['guj 윍Uj#Y] p҄5-/ܪ O-wxFt삖؆r^HK#Q6t0H{٫)DsYljOᛢTů20-Fq$t9`%YL!PSNŸP<}ԃ XAM{[5$! ~r4?F}yDŽ#wמ4􏍚f/v:m' kft[=>h:bow_ -s|ۊ6AG(#34Ume*lֈ[Gb}U8R'M}1t-=P$BM(-B\lQ^@w1øy:AC  =1^Zb_JտZ?L|S L+d/gh6Α4lNAFƓp`ط)`qܫ$Oԝ!(R>}4MY=qb8Ida#-ʣta8ʨ@gcYM-h-[W;=b%A_5]l#b={ z=ü `"ZZiSeO 2SY $%/X302j\0܂y*|rs,ja9͢hCv¿iO}\1ҜA6źjψ7 {!2YʹlzFŶjgv:' J^ߣߔ9|(5R*LmL/|H݂!kG -z6It7VR#p4-gCF@pO20Y%<*uygouZgYX'(M(Xr9NXT dnxcցbu$1-hS92M=>[fMs51As9rV&ixBo:u`r$5V~H-KzA?/X'9_F:PDsM Il}Nunpc9HH\I%&هt"׈Y>5?ƻT^s11p䡱ӠZ~ }㲘omd- `M,7Y2!raQ=E&goTyWZX3k5T9_C†pēA)|:eH 1(9=@f@ږmT84=*?&WLPݛjgvdE!dawŝ kwxcL cs. -V/jm9!Oefy6;߃y}h4abdXFVZHTVh+N!&GҒkBThF!9SGvyr9s :xo|mOIt'KLnB䏐Lp^)^zgϐFꅙ4<0L^9ݩlUEj5Y9h duFdȝ+ޔcla)F: Qn^. 9\`$oS²`M8P'hCUmyI0M6јk!pzYF8%W<)@#~y(QDa|D̡a+1e<ۯ]ŭ%dhnqјo[ xh#5k&gHң  ^7?]@w`G2!0`}oHO`0žwi2=o)Cy<ڂj>@OoW40V)'RwdF]TQ淬**L 81޼|ڑ 4/},jolJ׀?Tf7$LS{ySPӾ\S+}[ j<Xj+;[we䠈e%E7Es9swYB^獦_U\blI @?԰/GF[K# 9f5#upG"ܮ,~,~,L374^1I)3o&uXa5}EԾڢi߉!ydP|`1ߧ {Mr{D*7o^*}vDv# _ٵ:CAT5@ ;m靷/ Q uG$luc9-fzJSّT .7ryzCk@oא[Qǎ,ٹ2{wyjZeVEDL I}' "Dxj1vՌb:>3f 7#P29}mﻳŮߟf<aƆc>/Yg.|s>بZjAWX˃Fꔤ;5Lr.'_Gbա%z1@HgMT4eZ5P֡]Mk0B p=bMլNmmKO=+v )§#t|昬 ֶri8V{cU,xZ\qT:cP!/M7j ~IhC!ER;J8^QsD[іl 6l)U(b@Y$v(\'w*0-/lR7_T C6D\i*%) MP|k ٞWuB7r9+@CA_d A<;:\`W\}k0RuN{ˮƟXQbx-!I_ݒ~1cw}GUL<-qaquRu5e/g×尗#IVGo'B&LMufOtʻw Rf-KB+a! ߧ򣲙Ҁ~ (!]C )kL PYL 4G^ۥ$= p퓆9v{ΌH$gc"5AHc g HL+Q*y^OAH rCD*|زb[ ^ju-K[It L>+l<ԭ?rGy.Rw[6UOeM

V鴡Ӵ~3`gN&DtJoZ{fha B`w -ԛڄ}^L~1E( ƌPbg ]hM_Dt~8BN}ZۈB4b>g|. 9Pa-B?q@}<8 L23ˎ( shKF;jvO6)DV G^huR/v0X%+ic pV@ Bhh"!Ltvİhe;7GJwC\ ШavPHhU8OLIV,α/jvmE;>qJ]/n3O~]r{hو|AtCe0_ ŪN!;~=H[nnl'3hZqlSwA. 8 愾#d  oOgs[ivj1VDߝoEz(S\s=a$I#I[g&1{VbSTV^>t]"1 g<5 }ѵ9='=c T=1P,ۚ`A 7|`@Z qD x ݮ 3~f&N t>7|Y( 1.I G-^\hYgu®nLj0e,C{=̘_/mj'=G"0aIe`DD3"-^[̸6jB~7*8ԸnzZƑ0p|{F}\ A,{HS_Hmyzw {5(I~|^ԦT$ "BE:ߙ ?\K|^r+v?o_tI@܈pw:}8fOٽQx1E/7Ama&,쯲t ۣXwVbz U+j8*8{s D$Nk^lYKz^TD͊~}aNqu}yE6ea~?3X~o씾qO.8dyt u} |AbooQT|7|7|7|O.8dyt u} |AbooQT|7|7|7|߻C2}ۇg} F0 Hi= 6$O xA1*)BYARE_/uyM {/Q&q ,#HC4P(S1Oj<#`)rW[&@"5Guj_kRpD nX~i4Hm51ty=N:3QV_&yjQV_&yjlOX8Ap/O:CH]Dfo@UQ?[xYjp<1{{JFFB!ESs|iIKrI qP*,O_\Gĵ4wC0`3ʎLSS7O_nF]*8>st>HH6hЉ1Mة>8>LA9OHlP׮܃2i/M[Iu@ۥ90ʺr wP.5'ZQyrG:!Wp^D2H6< :)Z&ah3t\}N&GUH):ye5W"@3~u79t_ ܬguL,K$0?؎aQ,iTZțdH `J 5M뀀DEwP,$i3'u~CIdVcMOY HF#0W=;ܒ2)]q&?F2&),6]=ȌQufGoȨD^x[SԼ[,~s2Ң4IF>cfSBdaƾz}/mȚ_b!\ǛS|$e!屷:#|K?>zGY]!rus-0@~g]Rnz=”}+U? Ì_ZZjr%0*Ji$ }5JǪK4qxmRq1䞚gtTZ[r CƄ/@`KA'Xw-L$ȳb͆_ݦ`Nϖ??(N'^lj"Йu ?LC4,_\K4"F[}{T:{XȽ N_,#;_-+z1֠sn<ؐKOR%0.ԉCSѽЮ8Y1NgHϮ{a8/=;uosNzn:ML#7SZO*&Ji-Ҝ@lГo>E'bjZ'#@hb5rV{tAHY8BŒy`l%($Eg03973DhmS\^>&hś蜧Fime:FP|i'3Hsćq)}u'`l|c-Gd?z1F<A79Z~MZ@!wtmo|zP2&ljs3Y@J H3z)pjlV%tpY pğ&L]݉Y@5gϺϻ#3|< ]JH0j%Cw :=.T [W@/f[?%LѕhC!3'zi4$`j(s Kx|}(am զ'PfM 1 xHT^.vm5g0Y[(@mj>b\Tn_Z'ιK9@,a?;I F5b4w .YQ^D`ׯӱ>WS%+`I;9-b@spnD<Ìtj6ϑTG j LBgt67s A3d%H(ДKWg4wxL*.j33"ķ$2BU-3Snig@6̺@9tlvy[j['qhAỤV]x;䫑-&)Z@ɐsTu%0Nagstamon/Nagstamon/resources/nagstamon.ico000077500000000000000000000226761240775040100214510ustar00rootroot0000000000000000 %(0`               "*18?DJNR V!!X!! Z!! Z!!!Z!! Y W UQMIC=70(     &0;EO Y##"b'&%k**(t-,+|//-22044266487598699699788677565343111///-,,*z))'r&&$i#"!`VMC8. #   #:"""K###U%%$`((&k+*)u.-,00/432775::8>=;A@>DDBGGEJJHMLJONKNNKLLIJIGGFDCCA@@>=<;:9766433100.--+}**(s''&h%$$^###S"!!H4   ;;;lBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAA777JCCCCCCkkkUUUCCCCCCNCCC4CCC]]]CCCCCC CCCTTT}}}|||{{{DDDCCCPCCC}}}|||zzzYYYCCCnCCCoopqrstvwxyz{|}~~~~{{{{{{fffCCCoCCC*[}}}{{{gggCCCoCCCF~~~|||gggCCCoCCCEES~~~gggCCCoCCCEENNNhhhCCCoCCCEENhhhCCCoCCCEEFFMhhhCCCoCCCEEMhhhCCCoCCC&  HjjjCCCoCCCe___________________q_ju_________^6sk ooooooHjjjCCCoCCCjjffffffffffffffffffffffffffhfffffffffffffffWWGGjjjCCCoCCCppllllllllllllllllllllllllllllllllllllllllllllllRRRGGjjjCCCoCCCuusssssssssssssssssssssssssssssssssssssssssssssssssssccc<<<1FFjjjCCCoCCC{{zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzttaaQQDD::--OOkkkCCCoCCC~~~kkkCCCoCCC}}}kkkCCCoCCC{{{kkkCCCoCCClllCCCoCCCzz{lllCCCoCCC{{{{{~jjjCCCoCCCUUUCCCkCCC|KKKCCCCCCACCCCCCkkkQQQCCCCCCCCCRCCCRRRGGGCCCCCC)CCC)CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCNagstamon/Nagstamon/resources/nagstamon.png000077500000000000000000000041511240775040100214470ustar00rootroot00000000000000PNG  IHDR00WsRGBbKGD pHYs B(xtIME xP:IDAThKTY;v76݈ APc!6 0a˜٫ 3 1,n$ąqbIdmIx$PHuhh~Tzs*q|uι7fvG+@DY *L>l `dd`۶mhpε=I_Pƍ?0 ?F###CCC=cccY9 ZsVGN};SJfĉ]L& W$cǎH$uAlTa3o}Kؾ};Gɓ_ 7޽{YnoߦP( \.עgnMOO0mڴ 80pe4="ҹV %nS+ЋRPkT,F\X,#088뙙]Mr5wﲯo{Ӊ)΁1m9sk-bFD{͓m/NJ"ڵm6}qxuab1xɋURB2qx/B~;m3x>zX,, Kp!"E(w"6||DA G)M>S*R”lpM_ivcgmj "r,j!n79]UA?Y|Q7lekm%J` x9Ufy&T޿ 6nl0%} ҄eH \v5pE496vHy>;/; LT"WU/E>tzeaI.bff8{yApn \P'V7^_FuEK5:on1rtϴZ"ؖL=),7> '|t.\ No}m5Yh:;qJЮhG={ xgh"K:gi7f7VN&Ԣz*"G dڕ c#~q%VVmoOJ0?!س&&+<;@uR8CM1ws+WE8Ǽ_~z syW|%T^m7Go݂]|)DH7iX/*ى4!Y7Ϸ~_\#g 95->P8q38Ro> m '*'&uphp@{C3q8~VCulL1 0gZ߽Iav|ѝ:Ƙ@3x<իshIcYWKZ}@k]C>c|']{좠*H$_Dh|bb"Y/@^uo ׯV"=uݭZzZ{ZnӾZ'Dt\9km[4Ƅ1&0ɇ Ƙ1V[crؼs6g ssCcBPqdՒEbUyG{U!=k ђZEKDO)%(DU:猵l-[kJڂ1Z0oB:W00W. P#uI -T)%]"SJ<塔ES' s6֖JS0j0 KS/t ў'ZDP"ZF)sxZsN5ʜq:ce1V:1Zcl;AszFoXvjںIENDB`Nagstamon/Nagstamon/resources/nagstamon.svg000077500000000000000000000234701240775040100214670ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_acknowledged.png000066400000000000000000000015341240775040100241550ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYsT٪btEXtSoftwarewww.inkscape.org<IDATH_Has6r[$B#0n?D) s7yэ]HbI M PDvAuD tԹl֖;^}A4'"BVl˱p(14[MXzWtƌ(v9Y0ҏ}/1y&;oϼ}6X;AA\A琞k!:7E /1&;_.d'hɸ5X+,=M "ˀkT-o+C*lF:lqOzڵzIǑvl qHwΥ|ꇑĭE|UM*B>mg#-^D0{F.;5&;#uC:Jќ|y:pBJeѰv[\&椷\$wZ܌_/|D4~@s 5;~ }uL4/]I,q*#Xqح9 \ԋ4O%94~7!BfbPJ[?M6 LV,x$@4"~&x p^` 7WS<%< s!bl&Lj']f|∧P_ʊ/A[_MNJ@LDf1~EhVIENDB`Nagstamon/Nagstamon/resources/nagstamon_acknowledged.svg000066400000000000000000000137321240775040100241730ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_appindicator.png000066400000000000000000000020331240775040100241760ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsͺtEXtSoftwarewww.inkscape.org<IDAT8oE_WZRIU "U`@9D85pB8R G*.AZV(&T Hrm^  {qҫ}}ffWre}<99R"@k}F;Jw̍shRB)b)VJB0??Ú:8bfIZP=OJIT"0111mR):om& !Dؖdb12xKkR .>LuwhFW^׎^T( ӑ5VP%%Q)zD/"X"ZAl{[?B.ioqZk;ZC"ptXjcN74޽^GzohJCTWӈa8"Xxck4׆Yl{̎­n|M" G+!L D)a{llzxGx&ޤRkwa-VBbNtK/g_.Wy侜!Eaۜ=Vŗ\Zk@b0Z%χ9>_̓K)VhA /waC2r*J/ZK ̳!ۼH)\N}vz[pe y-I^/zZŋOqhţ׮=:U8gpl$pNۣt:V8C>6ccm00 1B ևVg) |VRdRlnn'Y^ |nfjshhSt0p]uFa:8ضeYXi뺌DX\\̋\.7 \fb1qIVK ggdQIENDB`Nagstamon/Nagstamon/resources/nagstamon_appindicator.svg000066400000000000000000000233411240775040100242160ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_black.png000077500000000000000000000013011240775040100225750ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs B(xtIME QQ\AIDAT8uJ#Qf&Š2Am+E!`+N,lk ,) Qt Ϲ[3sw?%maܒ$S image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_darkred.png000077500000000000000000000013231240775040100231410ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs B(xtIME h"mSIDAT8uJ[Qsr5BK Š+8rR CN*Hs_CHJ hMrSs5w4v}`u^TTR*> ic 1@׻];#_ZX(FyqLDQ(""4''',۶9"g JkR(&X+Zv`0dxxزDZ З*%<~1y=:gܟ{}M-'4;1FE/,`8Lb8 4:R-uf?VW1|:(кbpraADD"rNMZ<J1(<=2x33hx8:/. CDi[-WVЩ?'w*JӍ 4+jK%"h񗃥%n MOw Dy, \&hdYǺ Q>OvJl6MZH$>jAW:k?L&IRض i-,IENDB`Nagstamon/Nagstamon/resources/nagstamon_darkred.svg000077500000000000000000000224651240775040100231660ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_downtime.png000066400000000000000000000012241240775040100233500ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYsT٪btEXtSoftwarewww.inkscape.org<IDATH=QnbI%"Z`m_B6[` b) BtJۉ@Й9)aa~s. "J@ : Y,뚦R<) $I0d2uqt:TU=Ͽ ԇxh4hft]&J ZhqU Z뽫9Zkd!sx]}[9Kr image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_error.png000066400000000000000000000012631240775040100226560ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYsftEXtSoftwarewww.inkscape.org<0IDAT8};K$AVLM471061# EGyU]=յQ3.uSzۿ[s(pg_{bsspgg1Ɛ9ycb%IV$Iooo3;;y:A@aaa..q377G Ze((k-RJj5B>I0==RO~_)J)aH$ Cs$IBe\\\nsHe(h C image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_flapping.png000066400000000000000000000014131240775040100233220ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYsT٪btEXtSoftwarewww.inkscape.org<IDATHo@c/%)5)"DEP hhhhQ!D uJD($ r$ΝޡX.l9yEUGX-9EP)\9;zV뱥z2"/mZ!V'~5gmsP#\ 6aRf5/debøu[\MaCmC͊zBp)Qϫ 7=o H@"bTd#wbG\^Ҧd@U-`EaD(,UձQ%vhuX0'|Zʤ0=&&&͗ ^r A r|id gڶ &aѦ4 `0@"Sm oQ"EE!4 l>+@mÖadc}<"hz`r:$:\t049s"\vqzE image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_fresh.png000066400000000000000000000014121240775040100226300ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYsT٪btEXtSoftwarewww.inkscape.org<IDATH_hetnTK)?+$"[$, .Ⱥ.]tS^ CiT0M3asvřlso"BVI)5#"&,&[̂<""3-s8w )ORJ-y`SEC~)<_~TE*2.=9W&;vJعh[T(8:yV}**5ΡZM+q݋/km)*cvoAJ;0> Mlp {S(ik)FG(Q*Q4Q<Ȉ2]9ā^gG Ţ x*ҿ×KStr0mŪy?~hg.O^Esh[ų)z4h=C>qy f]ZY 7AJ/O:3O f&H)n`y5+fZqSJqqnFom=:a꯼s :#o s4pJ͸n 6{ĭ]hFʇ|rkkOl؏ulnHQ$fNKv ŃM/M]3VU,]o׌W7W 7nIENDB`Nagstamon/Nagstamon/resources/nagstamon_fresh.svg000066400000000000000000000162221240775040100226500ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_green.png000077500000000000000000000013131240775040100226240ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs B(xtIME #"NKIDAT8uOKQ2.Ң[p,įwM)t/ 4B)**fLjF3 ;wkU*Js>W^3c:9{'{{{߬ͰT*v 8YD(<Ţ.w1 RXJ[(,Ǥ oCFGGm[Dh60 \bH)H00.W+wL'ZD/ image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_label.png000077500000000000000000000061151240775040100226100ustar00rootroot00000000000000PNG  IHDR*\ҫsBIT|d pHYsޤtEXtSoftwarewww.inkscape.org< IDATx{pTgIH0hi|ЎP -hZS֩UQGmmU::ՑuFiU K"bx<6nf}7{Yv7Pw==|;QU||>+{>_n|) ߀|J7 ȧ$|) ߀|J"TI'41 ݝjݮC %O>Ӂ['L0mҤI\^SSMMM+U{~ߞϞ&M>=[qA"@Uqt:8C9T`0Ș1cF̞=ӮK,S"yP(tiظq#tx8NqGGGj*vYH0 " p<zyرuV&Oŋv p>"KrݑGKTW"q!tww3||(h@t1DbZGwpjVv[p- pPWq՗T|c(ʀT*ށkI ] |#ztYm#8e_ |A@wp ) ;8sguܲ6ovňgh6"R U[D@X<f`NZUUDBp[Ursy֩j^@HDF*F=Ga`n1*jǎHEU;<:+t"/0ب>PUH|jLorI&w@i7KkwD"u[%{E"28 c=(0 qf3CUӕd((?\ ]]n$ղifCUɘoCuu9VUmkVADì1f7Sk `4vFU;EpLj?`5[cȵC}=P^\U_C0/Avof%,d:tA)j hoM&3gr]ujj{W25"op~.&^%~=[ODT55ROU`PA^yx6KVUWi!;y;CcccScc|)|q)h@SN///bf]]A)* -ITu7-8ub}+VXngOQЀD|ذa+**jkCȲeee##p&U`(T!BQut*Nch*i image/svg+xml nagstamon Nagstamon/Nagstamon/resources/nagstamon_orange.png000077500000000000000000000013321240775040100230000ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs B(xtIME 6OmeZIDAT8uKQL23-QPPY-]!i[?@hѪe6n ʄLCǜ7z-^gЦxù9#2̓L& HHk-87B`fffĄ[,}5ZkRm1J)bw=ccNXH!R DH)|iJ\ia{{p/NWWUщN:Nt);` fn8Zk\c BWnE/ˋpP)a 1&X_Q;;^@W1VAGXkq **{P(@Of c}%FGhZ1 l22r ^Ө8 !DK5A )~fc= i1kmU/RmFP[NUą-͹RxG|<\:RBH)" Mx{믇11cuP%+AJ_ 'DU~gFqs\#N image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_passive.png000066400000000000000000000014121240775040100231730ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYsT٪btEXtSoftwarewww.inkscape.org<IDATHKa?ZEX֭RA? $C;t) At ::yH$-6BU+՜tݙmf\/<0>|>;3JDC); 0<>R: k6Mu"RJ#Rs5vTJ"AxODi S * pl" Y@u.v^_oMRDHm~8ahr; jX) 8 CbYF˅YZq۩ WE9aV:bxz>gYm6Wӽ/ƕ-4DX_W틭\/BD,.[x˴ѽl_#78a\{{sKP`bӽtȝ^BޅJ珶q"/ 46C3 +z8%(Qatb{oK9 SdJE""L)w%;* (2|ψ UD9*ka忣{T^^)+.ɖ=Eϥ^f~Ț|"2'DҨ1;=z~W]o97l 6`j "R%->TDe#IENDB`Nagstamon/Nagstamon/resources/nagstamon_passive.svg000066400000000000000000000161231240775040100232130ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_red.png000077500000000000000000000013141240775040100222770ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYsftEXtSoftwarewww.inkscape.org<IIDAT8u=KcQsn~Al4hRZ),dZ[#l,hPDL45ɽ9[DcxwQsss?''' u;szkP(TX*\^^C^^cmVb%LvK&]b.5ZkR(QJ5RhAk8"DW E qˁsx==֜$͖R]DJ"RV% CH1bkL image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_small.png000077500000000000000000000013441240775040100226400ustar00rootroot00000000000000PNG  IHDR?sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<aIDAT8kSA<VQV`qcp'N}K"…Q\BTGђE\vQS#6mzӼ\wE3w~sᨩ)î~;Oc<\PJR& @)њpzL$}X^ZvTEnۍ!`HX5jݜ,ymL+ۍ6[N~\벍9 בf1-W]fp3zbbW l9cO:ᬿoI'p@ӥ? t@8l|ne{i.]iP!8=ڄ=Mjk&55Ջ8UN8C)ET*d2R/, Y;1 ~i"$b0z |>lU0&" ~C#(5CܚGCṔtVkj06R cLբ{h`&'!Xl/̘fVW377O2 image/svg+xml Nagstamon/Nagstamon/resources/nagstamon_yellow.png000077500000000000000000000013541240775040100230440ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYsftEXtSoftwarewww.inkscape.org<iIDAT8uMOAO(M.z`HPBPI4ķB?^DϞ c`%N>H-a_ <|1fu9zLq:ho7nj.bǏeA`M߭=؋y9yG}|ֳ}d2 Z\\\Y"5Ǭ] ^8^r"A BAŢeT*bcXkEj,--mO6җ^*;IENDB`Nagstamon/Nagstamon/resources/nagstamon_yellow.svg000077500000000000000000000244411240775040100230610ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/recheckall.png000066400000000000000000000020271240775040100215520ustar00rootroot00000000000000PNG  IHDR1_sBIT|d pHYsnrtEXtSoftwarewww.inkscape.org<tEXtTitleView RefreshbtEXtAuthorJakub Steiner/!tEXtSourcehttp://jimmac.musichall.czif^)IDAT8eKh\e-3$ItjHlXlTZ񲐊VqYQFAJՅ * U()b.hiH2dr&Ic]w5[%!YP(eDa\wyt^\_eA926|I WUG*k65*T$$BuDkqc|nص}JGϟZR[):OJŖh%DiͶ h[Oi>88{}vc@d^Oۻ7sdjC!YMW 1+(pxfL$6~!c>T'O}l%NM0okxx:?oKTEf i֍L;?ZPݏ/2uj Ϟ~2xGwPO~'U6+t|`oe1ڵ"m1+uX-s2(?G_ UۡZ,Ӝ 0K>=u!Ok $=54ݴͅz%[pu&g))1ߞ2&O1wWzlۙ{4Ĵ=Swdrb* V8VfwꙤ~tTP<Ɯ^oK62Plof?H*_:KyݫUCv%uIyԄfPi,;_|1&40=6}ۿ 4ΞSF!@8GQ\/7Ɩ>jU&Zkqsp؀F$6v?@dŝ2wjKΫeIENDB`Nagstamon/Nagstamon/resources/recheckall.svg000066400000000000000000000443401240775040100215710ustar00rootroot00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz View Refresh reload refresh view Ricardo 'Rick' González Nagstamon/Nagstamon/resources/refresh.png000066400000000000000000000020451240775040100211130ustar00rootroot00000000000000PNG  IHDR1_sBIT|d pHYsnrtEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/!tEXtSourcehttp://jimmac.musichall.czif^UIDAT8]KL\e} PaHZBM7_tӐ-JLژb\4L01&>HNyAax9Ýasߝ&DI]n_礞4")R[z/jYeu9`8;qqDuB$O2fjb~ښn4F>@Hy8ܙNf=VHLWM]ºJ;̙֥Xuxa9 Yik?[V`,ӝKfff: NF#UqTU{3m[X8wνw|糱[O /Cd[m6S8<_%)=/~;hՆ띔 |!'N.73v?2'ߦW1w'^'+ |N4^T !K񩱾O0 ,=EWTwe&% >ͭ&n/HJu9V8q8 gh=xdr3}}zo,в[ *f!ġX:!di,-BrݎN/v]+fk hJѝ0I;;:?~an9-2ʹ"jDLA0t~04K?4+N~02"db-=dQW=}!Eng"Mr R湋}y`Ȱt  Nagstamon/Nagstamon/resources/services.png000066400000000000000000000017741240775040100213100ustar00rootroot00000000000000PNG  IHDRVΎWsBIT|d pHYs@ntEXtSoftwarewww.inkscape.org<yIDAT8uTOH#WL;d蘡ʮ$lf=K{0'= %J ăۃײГ"ފkc$4:d3$8yo/;n?x<ޟ}?9!lmmk6i^,,,|ǿ>do)tZ/Api@gmmmR۾#C0Eŧ>y(P,6 l6߅N>;;yyyUhuue4c||+G,\]]0<< b[ 1R1Vm^RQz@L&sX*8Vp4Oयin7j8(J{LB㒢(,˂mE1* B4c (eAQgx\"fuz h~r JLLL9=EkM^[PUu,%Ims*rJ19z$I@e);;cyw`avۦQu=L&gd29z!nۆa\yAY1D"onoo!2U׏@А >|((Lv9FR ˅n TU#T$RUUfR J)|>sd8s˲=,dߴVn3Ƽ 2lۆeY,"2:'Li J0=< >2m\.4wPoW`~~%\9R,pH\<>>󝹹IBHk{{c T*?888777\#<-p0fIENDB`Nagstamon/Nagstamon/resources/services.svg000066400000000000000000000155241240775040100213210ustar00rootroot00000000000000 image/svg+xml Nagstamon/Nagstamon/resources/settings.png000066400000000000000000000017541240775040100213230ustar00rootroot00000000000000PNG  IHDRDbsBIT|d pHYs;otEXtSoftwarewww.inkscape.org<tEXtAuthorAndreas Nilsson+GIDAT8[LgƟk,'iu,A؅(#AꅱR7&̘xnŘ-:IU(j6<.Dg㆕ƈXWJ}.g lO??C(Txܔk$hrU}gV*|M-)SJ۱W/#^qnO)榵.W?x|gX]m.jp77("ZV:GVaHؘ "]h>\ʜKϜ^/*,Y 1drv:`0^׭F=^رg_ܙY%ťC:Lh;Zk$`lM[l,~A@~jݺ$=R/@( H_ Ѱn3T oCRZAT+M+Dü9sEgp5<y+rM1A0tz-u87o?.KY׃XvGZFibR2|>?Yt-NoX~YsZsv%tYrV7‡NJND0X M'_~v%˅T~$}#T&k{au읞 Du@!L<f_[7ozf0j֒on=%q^91)bx؃Y*娽y3[U8Xݟwn,} 5m:Fa^x|1b_=|dT}qf+S 2"NhŒ jY]0eW<R&-k ,U aB/=qx oD)tٿ1q9!.G,֚-<IENDB`Nagstamon/Nagstamon/resources/settings.svg000066400000000000000000000662631240775040100213440ustar00rootroot00000000000000 image/svg+xml Andreas Nilsson category system preferences settings control center Jakub Steiner Ulisse Perusin image/svg+xml Preferences Andreas Nilsson Lapo Calamandrei, Ulisse Perusin, Jakub Steiner category system preferences settings control center Nagstamon/Nagstamon/resources/settings_action_dialog.ui000066400000000000000000001056661240775040100240370ustar00rootroot00000000000000 False Action settings False True center-always 320 260 True dialog True False True False end OK True True True True True True True False True 0 Cancel True False True False False True 1 False True end 0 True False 3 25 2 5 5 Enabled True False True False 0 True True False 0 Action type: 1 2 True False True Password for your Nagios website. 0 0 Target: 9 10 True False True False Host True True False 0 True True True 0 Service True True False 0 True True True 1 True True 0 1 2 9 10 Regular expressions for hosts True True False 0 True 2 11 12 True False True True 60 True False False True True True True 0 Help True True True True none 0 True True 1 1 2 6 7 True False True Username of your Nagios website. Username of your Nagios website. 0 String: 6 7 True False 0 2 Available variables for action strings: $HOST$ - host as in monitor $SERVICE$ - service as in monitor $MONITOR$ - monitor address $MONITOR-CGI$ - monitor CGI address $ADDRESS$ - address of host, delivered from connection method $USERNAME$ - username on monitor $STATUS-INFO$ - status information for host or service $PASSWORD$ - username's password on monitor $COMMENT-ACK$ - default acknowledge comment $COMMENT-DOWN$ - default downtime comment $COMMENT-SUBMIT$ - default submit check result comment $TRANSID$ - only useful for Check_MK as _transid=$TRANSID$ True 60 1 2 7 8 True False 0 2 Available action types: Browser: Use given string as URL, evaluate variables and open it in your default browser, for example a graph page in monitor. Command: Execute command as given in string and evaluate variables, for example to open SSH connection. URL: Request given URL string in the background, for example to acknowledge a service with one click. True 60 1 2 2 3 True False True False False False 0 Help True True True True none 0 False False 1 1 2 1 2 True False True URL of Nagios Webseite in your LAN. HTTPS is preferred. 0 Description: 5 6 True False 0 Name: 4 5 True True False False True True 1 2 5 6 True True 50 False False True True 1 2 4 5 True False 0 Monitor type: 3 4 See Python Regular Expressions HOWTO for filtering details. True True True True none 0 http://docs.python.org/howto/regex.html 2 17 18 Regular expressions for status informations True True False 0 True 2 15 16 True False False True True 20 True False False True True True True 0 reverse True True False 0 True False True 1 2 16 17 True False False True True 20 True False False True True True True 0 reverse True True False 0 True False True 1 2 14 15 Regular expressions for services True True False 0 True 2 13 14 True False False True True 20 True False False True True True True 0 reverse True True False 0 True False True 1 2 12 13 Close status popup window after action True True False 0 True 2 23 24 True False True Password for your Nagios website. Password for your Nagios website. 0 0 Status popup: 22 23 Leave status popup window open after action True True False 0 True True input_radiobutton_close_popwin 2 24 25 True False True False False True 0 1 2 3 4 False True 2 button_ok button_cancel Nagstamon/Nagstamon/resources/settings_dialog.ui000066400000000000000000006251431240775040100224770ustar00rootroot00000000000000 100 1 10 1 100 1 1 10 100 1 10 1 86400 1 10 True False nagstamon settings False center-always 320 260 True dialog False True False True False end OK True True True True True True True False False 0 Cancel True False True False False False 1 False True end 0 True False True True True False 5 6 2 5 True False never True False True False 0 2 True False 5 New server... True False True False True True 0 Edit server... True False False True True 1 Copy server... True False False True True 2 Delete server True False False True True 3 1 2 GTK_FILL GTK_FILL 5 5 True False 5 True False True How often Nagios status should be refreshed. How often Nagios status should be refreshed. 0 Update interval in seconds: False True 0 True True True False False True True adjustment_seconds False True 1 2 2 3 GTK_FILL True False 1 2 1 2 Check for new version at startup True False False 0 True 5 6 GTK_FILL Check for new version now True False False 1 2 5 6 GTK_FILL GTK_FILL True False 5 Debug mode True True False 0 True False True 0 Debug to file: True True False 0 True False True 1 True True True False False True True True True 2 2 4 5 GTK_FILL 6 Use system keyring for server credentials True True False 0 True 2 3 4 GTK_FILL Server False True False 5 17 2 True False 0 Statusbar display size: GTK_FILL 5 5 True False 0 Appearance: 2 3 GTK_FILL 5 5 True False 0 Statusbar details popup: 8 9 GTK_FILL 5 5 Short ("1 4") True False False 0 True 1 2 1 2 GTK_FILL 5 5 Show grid in status details overview True True False 0 True 2 12 13 GTK_FILL 5 5 True False 0 Default sort order: 16 17 GTK_FILL 5 5 True False 0 Default sort field: 15 16 GTK_FILL 5 5 True False 1 2 16 17 GTK_FILL 5 5 True False 1 2 15 16 GTK_FILL 5 5 Highlight new events in status details overview True True False 0 True 2 13 14 GTK_FILL 5 5 Show tooltips in status details overview True True False 0 True 2 14 15 GTK_FILL 5 5 Long ("1 WARNING 4 CRITICAL") True False False 0 True True input_radiobutton_short_display 1 2 GTK_FILL 5 5 Floating statusbar True False False 0 True True input_radiobutton_icon_in_systray 3 4 GTK_FILL 5 5 AppIndicator (Ubuntu & friends) True False False 0 True True input_radiobutton_icon_in_systray 4 5 GTK_FILL 5 5 True False True False Display to use: True True 0 True True True True 1 1 2 6 7 GTK_FILL 5 5 Popup when hovering over statusbar True False False 0 True True input_radiobutton_popup_details_clicking 9 10 GTK_FILL 5 5 Popup when clicking statusbar True False False 0 True True 10 11 GTK_FILL 5 5 Close when hovering out of popup True True False 0 True True input_radiobutton_close_details_clicking 1 2 9 10 GTK_FILL 5 5 Close when clicking statusbar True True False 0 True True 1 2 10 11 GTK_FILL 5 5 Icon in systray True False False 0 True True 5 6 GTK_FILL 5 5 Fullscreen True True False 0 True True input_radiobutton_icon_in_systray 6 7 GTK_FILL 5 5 True False True False Systray-popup-offset: True True 0 True True 2 True False False True True adjustment_hours True True 1 1 2 5 6 GTK_FILL 5 5 Display 1 True False 5 20 2 5 True False True Nagios will not be asked for the checked items. 0 Filter out the following: GTK_FILL All down hosts True True False 0 True 1 2 GTK_FILL All unreachable hosts True True False 0 True 2 3 GTK_FILL Acknowledged hosts & services True True False 0 True 1 2 1 2 GTK_FILL Hosts & services with disabled notifications True True False 0 True 1 2 2 3 GTK_FILL Hosts & services with disabled checks True True False 0 True 1 2 3 4 GTK_FILL Hosts & services down for maintenance True True False 0 True 1 2 4 5 GTK_FILL Hosts in soft state True True False 0 True 1 2 9 10 GTK_FILL Services on unreachable hosts True True False 0 True 1 2 8 9 GTK_FILL Services on hosts in maintenance True True False 0 True 1 2 7 8 GTK_FILL Services on down hosts True True False 0 True 1 2 6 7 GTK_FILL Services on acknowledged hosts True True False 0 True 1 2 5 6 GTK_FILL All warning services True True False 0 True 6 7 GTK_FILL All unknown services True True False 0 True 5 6 GTK_FILL All critical services True True False 0 True 4 5 GTK_FILL All flapping hosts True True False 0 True 3 4 GTK_FILL All flapping services True True False 0 True 7 8 GTK_FILL Regular expression for hosts True True False 0 True 2 11 12 GTK_FILL Services in soft state True True False 0 True 1 2 10 11 GTK_FILL See Python Regular Expressions HOWTO for filtering details. True True True True none 0 http://docs.python.org/howto/regex.html 2 19 20 GTK_FILL Regular expression for criticality (Centreon >= 2.4) True False 0 True 2 17 18 GTK_FILL True False True 40 True False False True True True True 0 reverse True False 0 True True True 1 2 18 19 GTK_FILL Regular expression for status informations True True False 0 True 2 15 16 GTK_FILL True False False True True 40 True False False True True True True 0 reverse True True False 0 True True True 1 2 16 17 GTK_FILL True False False True True 40 True False False True True True True 0 reverse True True False 0 True True True 1 2 14 15 GTK_FILL Regular expression for services True True False 0 True 2 13 14 GTK_FILL True False False True True 40 True False False True True True True 0 reverse True True False 0 True True True 1 2 12 13 GTK_FILL Filters 2 True False 5 6 2 5 True True never True True False 0 5 True False 5 New action... True True False True True 0 Edit action... True True False True True 1 Copy action... True True False True True 2 Delete action True True False True True 3 1 2 GTK_FILL 5 5 True False 1 2 1 2 True False 1 2 2 3 True False True False True False 0 Connection method: True True 0 True False True True 1 True False True True 2 True True 0 True False Host name True True False 0 True True True True 0 DNS name True True False 0 True input_radiobutton_connect_by_host True True 1 IP True True False 0 True input_radiobutton_connect_by_host True True 2 True True 1 5 6 GTK_FILL GTK_FILL 3 True False 5 3 5 Enable notification True False False 0 True True GTK_FILL True False 6 True False 2 Flashing statusbar/appindicator/systray icon True False False 0 True True Desktop notification True False False 0 True True 1 2 1 2 GTK_FILL True False 2 Enable sound (requires sox on unixoid OS) True False True False 0 True True True False 3 Use default Nagios sounds True False False 0 True 1 2 True False 2 Use custom sounds True False False 0 True input_radiobutton_notification_default_sound True False 3 4 True False 0 WARNING: 10 10 1 2 True False 0 CRITICAL: 10 10 1 2 1 2 True False 0 DOWN: 10 10 1 2 2 3 True False 2 False False False Choose sound file for WARNING 20 2 3 True False 2 False False Choose sound file for CRITICAL 20 2 3 1 2 True False 2 False False Choose sound file for DOWN 20 2 3 2 3 True False True False 2 True False gtk-media-play 3 4 True False True False 2 True False gtk-media-play 3 4 1 2 True False False 2 True False gtk-media-play 3 4 2 3 1 2 2 3 Repeat sound until notification has been noticed True False True False 0 True 1 2 2 3 GTK_FILL True False True False True False 0 0 Notifications: True True 0 True True 0 True False WARNING True False False 0 True True True 0 CRITICAL True False False 0 True True True 1 UNKNOWN True False False 0 True True True 2 UNREACHABLE True False False 0 True True True 3 DOWN True False False 0 True True True 4 True True 1 GTK_FILL True False Notification actions True True False True False True 0 Help True True True True none False True 1 3 4 GTK_FILL GTK_FILL True False True False 4 3 5 True True True False False True True 2 3 True True True False False True True 2 3 1 2 True True True False False True True 2 3 2 3 WARNING True True False True GTK_FILL GTK_FILL CRITICAL True True False True 1 2 GTK_FILL GTK_FILL DOWN True True False True 2 3 GTK_FILL GTK_FILL OK True True False True 3 4 GTK_FILL GTK_FILL True True True False False True True 2 3 3 4 True True 1 True False Custom notification action True True False True False True 0 Help True True True True none False True 1 True True 3 False 0 0 5 A custom notification will be used for every notification event. The notification message will be stored in the $EVENTS$ variable. Example: zenity --notification --text "$EVENTS$" The events will be separated by the separator. As default all events of one refresh cycle will be used in one notification action. If you want an action for every single event enable the option "Run one single action for every event". True 60 True True 4 True False 3 2 5 True False 0 Action string: GTK_FILL True False 0 Event separator: 1 2 GTK_FILL True True True False False True True 1 2 True True True False False True True 1 2 1 2 Run one single action for every event True True False True 2 2 3 True True 5 5 6 GTK_FILL False 0 0 5 Every state allows to trigger some specific custom action like playing some sound, open some special application or whatever. These actions are triggered by the worst state. Just specify a command line for the desired action. True 60 4 5 GTK_FILL GTK_FILL 1 2 GTK_FILL 4 True False 5 2 5 True False 0 Set colors for text and background of status information: GTK_FILL True False 10 3 5 5 True False 0 OK: 1 2 GTK_FILL True False 0 WARNING: 2 3 GTK_FILL True False 0 CRITICAL: 3 4 GTK_FILL True False 0 UNKNOWN: 4 5 GTK_FILL True False 0 UNREACHABLE: 5 6 GTK_FILL True False 0 DOWN: 6 7 GTK_FILL True False 0 ERROR: 7 8 GTK_FILL True False 0 Text: 10 1 2 GTK_FILL True False 0 Background: 10 2 3 GTK_FILL True True False #000000000000 1 2 1 2 GTK_FILL True True False #000000000000 1 2 2 3 GTK_FILL True True False #000000000000 1 2 3 4 GTK_FILL True True False #000000000000 1 2 4 5 GTK_FILL True True False #000000000000 1 2 5 6 GTK_FILL True True False #000000000000 1 2 6 7 GTK_FILL True True False #000000000000 1 2 7 8 GTK_FILL True True False #000000000000 2 3 1 2 GTK_FILL True True False #000000000000 2 3 2 3 GTK_FILL True True False #000000000000 2 3 3 4 GTK_FILL True True False #000000000000 2 3 4 5 GTK_FILL True True False #000000000000 2 3 5 6 GTK_FILL True True False #000000000000 2 3 6 7 GTK_FILL True True False #000000000000 2 3 7 8 GTK_FILL True False 5 True Reset to previous True True False True True 0 Defaults True True False True True 1 3 9 10 10 1 2 GTK_FILL 5 True False 5 14 2 5 True False 0 0 Set some default settings for default actions: 2 GTK_FILL True False 0 Acknowledgements: 1 2 GTK_FILL 5 Sticky acknowledgement True True False 0 True 2 3 GTK_FILL 5 Send notification True True False 0 True 3 4 GTK_FILL 5 Persistent comment True True False 0 True 4 5 GTK_FILL 5 Acknowledge all services on host True True False 0 True 5 6 GTK_FILL 5 True False 0 Submit check result: 12 13 GTK_FILL True False 5 True False 0 5 Comment: False True 0 True True 30 True False False True True True True 1 2 13 14 GTK_FILL True False 5 True False 0 5 Comment: False True 0 True True 30 True False False True True True True 1 10 11 GTK_FILL True False 5 True False 0 5 Duration: False True 0 True False True True 2 True False False True True adjustment_hours True True 0 True False 0 6 hours True True 1 True True 2 True False False True True adjustment_minutes_default True True 2 True False 0 5 minutes True True 3 False True 1 2 9 10 GTK_FILL True False 0 Downtime: 8 9 GTK_FILL True False 5 True False 0 5 Comment: False True 0 True True 30 True False False True True True True 1 2 6 7 GTK_FILL True False True False 0.0099999997764825821 6 Type: False True 0 True False Fixed True True False 0 True True False True 0 Flexible True True False 0 True input_radiobutton_defaults_downtime_type_fixed False True 1 False True 1 7 8 GTK_FILL 6 True True end 1 button_ok button_cancel Nagstamon/Nagstamon/resources/settings_server_dialog.ui000066400000000000000000000630271240775040100240620ustar00rootroot00000000000000 False Server settings False True center-always True dialog True True False True False end OK True True True True True True True False True 0 Cancel True False True False False True 1 False True end 0 True False 3 18 2 5 5 Enabled True False True False 0 True 2 True False 0 Type: 1 2 True False 0 Name: 2 3 True False True URL of Nagios Webseite in your LAN. HTTPS is preferred. 0 Monitor URL: 3 4 True False True HTTP and HTTPS possible; e.g. &quot;https://nagios-server/nagios/cgi-bin&quot;. HTTPS is by all means the preferred because more secure option. Don&apos;t put any status.cgi in there. 0 Monitor CGI URL: 4 5 True False True Username of your Nagios website. 0 Username: 5 6 Save password True False True False True 2 6 7 True False True Password for your Nagios website. 0 Password: 7 8 Use proxy True False True False True 2 10 11 Use proxy from OS True False False True 2 11 12 True False 0 Proxy address: 15 12 13 True False 0 Username: 10 13 14 True False 0 Password: 10 14 15 True True True 60 Default False False True True 1 2 2 3 True True True https://monitor-server False False True True 1 2 3 4 True True True https://monitor-server/monitor/cgi-bin False False True True 1 2 4 5 True True True user False False True True 1 2 5 6 True True True False password False False True True 1 2 7 8 True True 30 http://proxy:port/ False False True True 1 2 12 13 True True True 25 proxyuser False False True True 1 2 13 14 True True True False 25 proxypassword False False True True 1 2 14 15 Use autologin True True False True 2 8 9 True False 0 Autologin Key: 9 10 True True False False True True 1 2 9 10 True False True False False False 0 1 2 1 2 Use display name as host name True False False 0 True 2 15 16 Use display name as service name True False False 0 True 2 16 17 True False 2 button_ok button_cancel Nagstamon/Nagstamon/resources/submit_check_result_dialog.ui000066400000000000000000000456101240775040100246700ustar00rootroot00000000000000 False Submit check result False True center-always 318 260 normal True False True False 14 2 True False 0 Check result: 2 3 5 5 OK True True False 0 True True 1 2 2 3 5 5 WARNING True True False 0 True input_radiobutton_result_ok 1 2 4 5 5 5 CRITICAL True True False 0 True input_radiobutton_result_ok 1 2 5 6 5 5 UNREACHABLE True True False 0 True input_radiobutton_result_ok 1 2 6 7 5 5 UNKNOWN True True False 0 True input_radiobutton_result_ok 1 2 7 8 5 5 DOWN True True False 0 True input_radiobutton_result_ok 1 2 8 9 5 5 False 0 3 Check output: 10 11 5 5 True dummy check output False False True True 1 2 10 11 5 5 False 0 3 Performance data: 11 12 5 5 UP True True False 0 True True input_radiobutton_result_ok 1 2 3 4 5 5 Change submit check result defaults... True True True 1 2 13 14 5 5 False 0 Comment: 12 13 5 5 True False False True True 1 2 11 12 5 5 True False False True True 1 2 12 13 5 5 True False 0 description - set by GUI.py True 2 1 2 5 5 True False hidden hostname True False hidden service 1 2 False True 0 True False end OK True True True True True False False 0 Cancel True False False False False 1 False True end 1 button_ok button_cancel Nagstamon/Nagstamon/resources/warning.wav000066400000000000000000000213721240775040100211370ustar00rootroot00000000000000RIFF"WAVEfmt ++data"xvzs}zip{~fvwv|x~}wuj}}ou{|vywwzxh||}xpn}|ut|~y~it|uwt||s{s~nqv{|xyqvxn}}vsjxx|su~vpwtoq|swutqw~v{isv~xqz|}upvsostz{{pxswyyy{poq{q}v}~|rvwusivz}xs{yntu}vsjwyvxwrtoztkvv||{v~vtg}vkoy{q~rop|kt|w|v}}~skm{yy~pm|~|vvwxmkwvo|~uz|x}jjvx|z~kz~uxyv{rgrxtv~|||wzzrdryr~xsts{yxxxhnxwzzz~~{r|qkjvww{|{|ntvyx{klrz}{~|v{~wrvkllxu~rmqvzxukpw~{}vtzrqpklutzvljpxz|pot}~~xqwtponnpyx|pgis{~}wrt|}{|wqp}{plooqx~x~sidmx~|zxv{zvvqnusmlsrxy}||}~vkdft|{{{{{zupqlq~znmpwv{u|z~wofclzzz|tmklkx~umpv{|xrsy|{pkcgt{zz{liijqztox|{oorxsoggpy|yyrfgemxzrx~zojlrxqlgnt~yzw|wgdcht|xz|xnghjyvrlpty~|vwx{~mfbdpv|xnfgfr|wpsvv}zyrtyzskdalr{yodgek|vwyuzzvwppxxzqkbipt}sefdft||y~xyzsvpksvxsggmoz|}yhfecnv|}{zqsphor{}zmhljtxvnfeajpw||pqqhlmtskkgqvr|}thf_gko~spqhkimznnclvpxy|}nh^cjiyzqqijidv|upifsotxuvm_^ggp~wtjgj`kxtvnfopsvpx~yvgRWYf}~nRP[D=LZvʲ~lZH=AFV_[+(;@TtǷ{m\WCImȬ_TQ7C^o|rqptzpPD1Njlx~xUAJZCQa{qZcbZfqqvuZkd|w}{hg}nin|fcwxv|`qxyt_^QRtssyreRa^`U}xneWIdbavl]^Ykmjvx`c[UP}tnYbk_d~t|bof{~ty~~tjnbXekfn|\`LYcjendcgpws\\xzza\l]qwx~~ogfnmhm~u}qkk|zg~}egy`WltxsWMT\cko~pelvgiiTct~^`krsvrjq}cgmgjkl~~xzsyytq~s]j~g`n~sNKY``ur~rituZ\__f}xh[qyoah\x|dnodkjrp{z}{~vwx~ywkcr~ukx||whQTc_lu|nrrgNS_qij`ww_XYd~vunm`kixzvn{px~y~zvfpky|py|z}pof__cdp|otmnXQIous{pgsp\RUlv|sdc`olmywt|q~w{wpghqws~xvniipcdgrppj`XMTs{{~zvpiiZR\ktna]bk_do|~~wsw}kc_jvsw~{{vumgvtifl|~rl_YRP_szv~~oec\Y_frl[\b`X[ktxvf[\lqtxwqqptw}}pnwyplqvdYRQT^nwrrc`__Z`nl][]WQSg~~}|yyqbT[dnsniknnmsvuuy|xs^POQW_txqu~rccge\etp_ZTNHSi~}vz~zpl]VWep|xgchgcckt}|rVKMSXf|zxwspwy|sifmlgh|ybSLCCNe}|tsvwlgbWWdt~hab^WXaqyv}rWHNU]mwquspqsssmjqxsscNFAAOf{yurx~qccc\\mk^[UMQ_p|spyylWLS`i{tovslmmnttq{{~`HDDFWpw{wv{{yxlaagffwiWTOJSbv|ypozxol\R_nwtntohjkpw|||~y_HIKQcyv}{yxurpmbanqn~eQPMNWi~zvpqypjnc]l{tmnkgipr{{|||t`OPV`n~y~vtrnmpcfu{xx`OMTQ`tuutsy}wy{jkmnjzx}ughkhlwvvxzswcZUhjz{qtpnksepvxyocOSXXem{rwszhxrsftw{|vsgdrfvs|zyyo}yts\cpv|qttmmnso~rnhUY\^nszxtppkpvsgxyw~qeirmsxvwpt}x{tdnv{y{stungtzv~qrl\Z]fqw{|qjplnvqm}~}zyphmsnm~qnqs}|zups||sz}uqukgx|~vvp`Uajq|~~lgqnlsrtz{vvmjqijkluu~~~u{xu~xq|y~woqjn{zy{uq^Uels~zjhopimx}wvwlhigmzjpzy{}xw~xzrs{|zz{sijquz}|{}ytiZ\iqqz|sihnncp{wvvhaajsupuys|v|vsut|~zvwock|||wu}|nd_`kslwsrkfkkes~}ysqdY`pv|xzvn|~|ppyw|xpribqsuxfeedmojz~mpkdghnz}wnj\Vgt{~}zooz{wxmu}}|{pjiejz~|s~ndjigjlqrknjbcpxysibW\my{wqoox~vu}spy~wvmdcjr}|zylhmmgfpy~qjlkadu~||~{rd\Z`mzy|omposxsx}wuxxspi_cqx|{qmmpofhzvoikhaj{~{|{wl^[`fqyyvimqooquzxw}{uolc\gw}~y~snpqsohqxsnhgfgp~|{ysg\_fjpxpglqnhnw}~x}{vrlg_^k{{yyz~xoptvupq{|trngchov~wlb_fmnq{xmilpieo{~{}vrmf`_frwswwvtqpuyywx}trqlccnw|{||shcgosqttnjkjfgs~~~{upjb`fmw}pnstolovz~||xrppibgs{wywohipwxuyysokgfgnxzvtnf`ent{ukkppiks{soolggp}|zwvvrppuy}{z|urogdgnw}yuuskdenwy{zmhhljio||vronkknxyssvtsqvz~~~vssogemv~ustsmhlv}z{}ukgghimvzvstnklqv}|qmnttst~urtojir|}yttussot{~yzwpjgfflt~~urtvolqw}~sjjotuw||utspmpzzwvwwuvxwz||y{{qnlgfiq|~xsqwwppv~|wpklquy}~}~|xttssv~{vtw{{wz~~{yzy}yoonihnv~xwsuwxtrx~}tpkhmqv}||{|~{tuvw{}utuz~|}ywyz~woqojmr|}ywxxwwywu|~}~yplllpsy}{{z||twz|~~||xtty}wvx|~xrqpnpv~zywz}xwzzwz|~wnloopv|}z|~~{}|vw|~}zxwvw}~wvy}|ztprqsz}}{zy}~xwz|xx|~wnmqpry}|~}{~~|||~|wx~~~wwxw{xv{|}|srttx|}}|{|}}zwz}xw}~woprqu{|~~}~~}~}z|~y{~~wwyy|~~~xvz|~}|tsvwy{~~|}|{}{wy|xw|~}xqprtv|~~~~}||x{~{{}~xwxzz~~}~zxz~|}{wtwyy|~~|}{z~|xz{yx|~{xrpsvx~~~{z}{x{~|z~~wvyy{~|~|y~{{|xvzzy}~~|||z{}|xx{{x}|yxsrvwx}~}|yy{|yz~|{~~xvxy{||{z~zwyyz|~~|zz{{|}yx||z~{xxutvwxz~~{zywz}zz}||~~}|wuwy{}~~}~~z|~{xx{{|||zx{|z{|yz}~}zxxwvvww{|xxww{|yz|~}~~|zvtxyy|}}~}{}}|||{wz||~~yzxx{|y{}||~zxzxuuvw|~~zwwxx{|zy}~zyvtwxwy}~}}|y{~{{zx{}}|wwwx{|z{zyzyttvx|}|xuvyzz|{z}xwuuvwvy~~~|{zzy{}zz{{}~xuuxxy|{~}}{zzwtuy|{yvuw{zz{|~|vvuutvw{|||yxxy{~|zz}~}{vsuxxy|~~~|||{ywvw|~~ywuux|{{}zuvvtsvx~{{zwvx{|}|{|zvsrvxyz~}}}~|z|}{yxx{~{vtvxz||}|xvwvttx}|zzxuvz||||}|vsstwyz||{|||{z{~~{yz|~~|xttx{|~~}yxwwvtv{~|yxvtw{~||}~ytrsuwy|~zz|{yy{}~|{}~|yvsvz}}~{yyyywvy~}{xvuvy}~}}~{ustwwy{zxzzxwy}}|~}|yvtvz~~~~|z{{{ywy}}|zvuwz|~}~|wttwxxy}|yxyywx{}~~|zxvvx|}|~~}{|}}|zy{~~}|yvuy|~}}~~{wuwyzyz~{yxxwwy}~}}{xwwy|~}z{~~}{|~{{|~~}}|xvy|~~|}~|yxxz|zy||{yxwwy|~~|||zwwz|~~~zz{}}||}|}~}|{yy|~}|~~}{zz{{|zz~}{{zxww{~~|{||zwx|~~~~|zz{|||}~|~~~}|{z{}~}|}}~}z|}||{{||||{xwy|~~}}||||zy{~~}|{zz{{{}~}~}~||}~~~|~|}~|{~}{{|~}}}{wx{}~}}}|}||{{{|~~|}~{{{{{z|~}}~~{|~~}||~|}~|}~z{~}|zxz|}}}~~|}~~|{}}}~}|~}{||{zz|~~~~}|~~||}~}|~~~}~}z|~~}~}{zz{|||~|}~~{|~}||}|{||zy{~~~~~}|~}{|~~}~~~~}~}||~}|~|z{{{||}~}~~~|}|{|~~~|||yy|~~~~~~}~~|{}}|~~~}|~~}|~||{z|||{{}}~~~~~}~{z|~~|{zy{}~~~~}~}}|}~|}}}||~}~~||}|~~z{||{{|~~~~~~~~}zz}~~~{zzz|}~~Nagstamon/Nagstamon/thirdparty/000077500000000000000000000000001240775040100171265ustar00rootroot00000000000000Nagstamon/Nagstamon/thirdparty/BeautifulSoup.py000066400000000000000000002331301240775040100222710ustar00rootroot00000000000000"""Beautiful Soup Elixir and Tonic "The Screen-Scraper's Friend" http://www.crummy.com/software/BeautifulSoup/ Beautiful Soup parses a (possibly invalid) XML or HTML document into a tree representation. It provides methods and Pythonic idioms that make it easy to navigate, search, and modify the tree. A well-formed XML/HTML document yields a well-formed data structure. An ill-formed XML/HTML document yields a correspondingly ill-formed data structure. If your document is only locally well-formed, you can use this library to find and process the well-formed part of it. Beautiful Soup works with Python 2.2 and up. It has no external dependencies, but you'll have more success at converting data to UTF-8 if you also install these three packages: * chardet, for auto-detecting character encodings http://chardet.feedparser.org/ * cjkcodecs and iconv_codec, which add more encodings to the ones supported by stock Python. http://cjkpython.i18n.org/ Beautiful Soup defines classes for two main parsing strategies: * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific language that kind of looks like XML. * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid or invalid. This class has web browser-like heuristics for obtaining a sensible parse tree in the face of common HTML errors. Beautiful Soup also defines a class (UnicodeDammit) for autodetecting the encoding of an HTML or XML document, and converting it to Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. For more than you ever wanted to know about Beautiful Soup, see the documentation: http://www.crummy.com/software/BeautifulSoup/documentation.html Here, have some legalese: Copyright (c) 2004-2010, Leonard Richardson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the the Beautiful Soup Consortium and All Night Kosher Bakery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. """ from __future__ import generators __author__ = "Leonard Richardson (leonardr@segfault.org)" __version__ = "3.2.0" __copyright__ = "Copyright (c) 2004-2010 Leonard Richardson" __license__ = "New-style BSD" from sgmllib import SGMLParser, SGMLParseError import codecs import markupbase import types import re import sgmllib try: from htmlentitydefs import name2codepoint except ImportError: name2codepoint = {} try: set except NameError: from sets import Set as set #These hacks make Beautiful Soup able to parse XML with namespaces sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match DEFAULT_OUTPUT_ENCODING = "utf-8" def _match_css_class(str): """Build a RE to match the given CSS class.""" return re.compile(r"(^|.*\s)%s($|\s)" % str) # First, the classes that represent markup elements. class PageElement(object): """Contains the navigational information for some part of the page (either a tag or a piece of text)""" def setup(self, parent=None, previous=None): """Sets up the initial relations between this element and other elements.""" self.parent = parent self.previous = previous self.next = None self.previousSibling = None self.nextSibling = None if self.parent and self.parent.contents: self.previousSibling = self.parent.contents[-1] self.previousSibling.nextSibling = self def replaceWith(self, replaceWith): oldParent = self.parent myIndex = self.parent.index(self) if hasattr(replaceWith, "parent")\ and replaceWith.parent is self.parent: # We're replacing this element with one of its siblings. index = replaceWith.parent.index(replaceWith) if index and index < myIndex: # Furthermore, it comes before this element. That # means that when we extract it, the index of this # element will change. myIndex = myIndex - 1 self.extract() oldParent.insert(myIndex, replaceWith) def replaceWithChildren(self): myParent = self.parent myIndex = self.parent.index(self) self.extract() reversedChildren = list(self.contents) reversedChildren.reverse() for child in reversedChildren: myParent.insert(myIndex, child) def extract(self): """Destructively rips this element out of the tree.""" if self.parent: try: del self.parent.contents[self.parent.index(self)] except ValueError: pass #Find the two elements that would be next to each other if #this element (and any children) hadn't been parsed. Connect #the two. lastChild = self._lastRecursiveChild() nextElement = lastChild.next if self.previous: self.previous.next = nextElement if nextElement: nextElement.previous = self.previous self.previous = None lastChild.next = None self.parent = None if self.previousSibling: self.previousSibling.nextSibling = self.nextSibling if self.nextSibling: self.nextSibling.previousSibling = self.previousSibling self.previousSibling = self.nextSibling = None return self def _lastRecursiveChild(self): "Finds the last element beneath this object to be parsed." lastChild = self while hasattr(lastChild, 'contents') and lastChild.contents: lastChild = lastChild.contents[-1] return lastChild def insert(self, position, newChild): if isinstance(newChild, basestring) \ and not isinstance(newChild, NavigableString): newChild = NavigableString(newChild) position = min(position, len(self.contents)) if hasattr(newChild, 'parent') and newChild.parent is not None: # We're 'inserting' an element that's already one # of this object's children. if newChild.parent is self: index = self.index(newChild) if index > position: # Furthermore we're moving it further down the # list of this object's children. That means that # when we extract this element, our target index # will jump down one. position = position - 1 newChild.extract() newChild.parent = self previousChild = None if position == 0: newChild.previousSibling = None newChild.previous = self else: previousChild = self.contents[position-1] newChild.previousSibling = previousChild newChild.previousSibling.nextSibling = newChild newChild.previous = previousChild._lastRecursiveChild() if newChild.previous: newChild.previous.next = newChild newChildsLastElement = newChild._lastRecursiveChild() if position >= len(self.contents): newChild.nextSibling = None parent = self parentsNextSibling = None while not parentsNextSibling: parentsNextSibling = parent.nextSibling parent = parent.parent if not parent: # This is the last element in the document. break if parentsNextSibling: newChildsLastElement.next = parentsNextSibling else: newChildsLastElement.next = None else: nextChild = self.contents[position] newChild.nextSibling = nextChild if newChild.nextSibling: newChild.nextSibling.previousSibling = newChild newChildsLastElement.next = nextChild if newChildsLastElement.next: newChildsLastElement.next.previous = newChildsLastElement self.contents.insert(position, newChild) def append(self, tag): """Appends the given tag to the contents of this tag.""" self.insert(len(self.contents), tag) def findNext(self, name=None, attrs={}, text=None, **kwargs): """Returns the first item that matches the given criteria and appears after this Tag in the document.""" return self._findOne(self.findAllNext, name, attrs, text, **kwargs) def findAllNext(self, name=None, attrs={}, text=None, limit=None, **kwargs): """Returns all items that match the given criteria and appear after this Tag in the document.""" return self._findAll(name, attrs, text, limit, self.nextGenerator, **kwargs) def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): """Returns the closest sibling to this Tag that matches the given criteria and appears after this Tag in the document.""" return self._findOne(self.findNextSiblings, name, attrs, text, **kwargs) def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, **kwargs): """Returns the siblings of this Tag that match the given criteria and appear after this Tag in the document.""" return self._findAll(name, attrs, text, limit, self.nextSiblingGenerator, **kwargs) fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x def findPrevious(self, name=None, attrs={}, text=None, **kwargs): """Returns the first item that matches the given criteria and appears before this Tag in the document.""" return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, **kwargs): """Returns all items that match the given criteria and appear before this Tag in the document.""" return self._findAll(name, attrs, text, limit, self.previousGenerator, **kwargs) fetchPrevious = findAllPrevious # Compatibility with pre-3.x def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): """Returns the closest sibling to this Tag that matches the given criteria and appears before this Tag in the document.""" return self._findOne(self.findPreviousSiblings, name, attrs, text, **kwargs) def findPreviousSiblings(self, name=None, attrs={}, text=None, limit=None, **kwargs): """Returns the siblings of this Tag that match the given criteria and appear before this Tag in the document.""" return self._findAll(name, attrs, text, limit, self.previousSiblingGenerator, **kwargs) fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x def findParent(self, name=None, attrs={}, **kwargs): """Returns the closest parent of this Tag that matches the given criteria.""" # NOTE: We can't use _findOne because findParents takes a different # set of arguments. r = None l = self.findParents(name, attrs, 1) if l: r = l[0] return r def findParents(self, name=None, attrs={}, limit=None, **kwargs): """Returns the parents of this Tag that match the given criteria.""" return self._findAll(name, attrs, None, limit, self.parentGenerator, **kwargs) fetchParents = findParents # Compatibility with pre-3.x #These methods do the real heavy lifting. def _findOne(self, method, name, attrs, text, **kwargs): r = None l = method(name, attrs, text, 1, **kwargs) if l: r = l[0] return r def _findAll(self, name, attrs, text, limit, generator, **kwargs): "Iterates over a generator looking for things that match." if isinstance(name, SoupStrainer): strainer = name # (Possibly) special case some findAll*(...) searches elif text is None and not limit and not attrs and not kwargs: # findAll*(True) if name is True: return [element for element in generator() if isinstance(element, Tag)] # findAll*('tag-name') elif isinstance(name, basestring): return [element for element in generator() if isinstance(element, Tag) and element.name == name] else: strainer = SoupStrainer(name, attrs, text, **kwargs) # Build a SoupStrainer else: strainer = SoupStrainer(name, attrs, text, **kwargs) results = ResultSet(strainer) g = generator() while True: try: i = g.next() except StopIteration: break if i: found = strainer.search(i) if found: results.append(found) if limit and len(results) >= limit: break return results #These Generators can be used to navigate starting from both #NavigableStrings and Tags. def nextGenerator(self): i = self while i is not None: i = i.next yield i def nextSiblingGenerator(self): i = self while i is not None: i = i.nextSibling yield i def previousGenerator(self): i = self while i is not None: i = i.previous yield i def previousSiblingGenerator(self): i = self while i is not None: i = i.previousSibling yield i def parentGenerator(self): i = self while i is not None: i = i.parent yield i # Utility methods def substituteEncoding(self, str, encoding=None): encoding = encoding or "utf-8" return str.replace("%SOUP-ENCODING%", encoding) def toEncoding(self, s, encoding=None): """Encodes an object to a string in some encoding, or to Unicode. .""" if isinstance(s, unicode): if encoding: s = s.encode(encoding) elif isinstance(s, str): if encoding: s = s.encode(encoding) else: s = unicode(s) else: if encoding: s = self.toEncoding(str(s), encoding) else: s = unicode(s) return s class NavigableString(unicode, PageElement): def __new__(cls, value): """Create a new NavigableString. When unpickling a NavigableString, this method is called with the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be passed in to the superclass's __new__ or the superclass won't know how to handle non-ASCII characters. """ if isinstance(value, unicode): return unicode.__new__(cls, value) return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) def __getnewargs__(self): return (NavigableString.__str__(self),) def __getattr__(self, attr): """text.string gives you text. This is for backwards compatibility for Navigable*String, but for CData* it lets you get the string without the CData wrapper.""" if attr == 'string': return self else: raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) def __unicode__(self): return str(self).decode(DEFAULT_OUTPUT_ENCODING) def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): if encoding: return self.encode(encoding) else: return self class CData(NavigableString): def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): return "" % NavigableString.__str__(self, encoding) class ProcessingInstruction(NavigableString): def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): output = self if "%SOUP-ENCODING%" in output: output = self.substituteEncoding(output, encoding) return "" % self.toEncoding(output, encoding) class Comment(NavigableString): def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): return "" % NavigableString.__str__(self, encoding) class Declaration(NavigableString): def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): return "" % NavigableString.__str__(self, encoding) class Tag(PageElement): """Represents a found HTML tag with its attributes and contents.""" def _invert(h): "Cheap function to invert a hash." i = {} for k,v in h.items(): i[v] = k return i XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", "quot" : '"', "amp" : "&", "lt" : "<", "gt" : ">" } XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) def _convertEntities(self, match): """Used in a call to re.sub to replace HTML, XML, and numeric entities with the appropriate Unicode characters. If HTML entities are being converted, any unrecognized entities are escaped.""" x = match.group(1) if self.convertHTMLEntities and x in name2codepoint: return unichr(name2codepoint[x]) elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: if self.convertXMLEntities: return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] else: return u'&%s;' % x elif len(x) > 0 and x[0] == '#': # Handle numeric entities if len(x) > 1 and x[1] == 'x': return unichr(int(x[2:], 16)) else: return unichr(int(x[1:])) elif self.escapeUnrecognizedEntities: return u'&%s;' % x else: return u'&%s;' % x def __init__(self, parser, name, attrs=None, parent=None, previous=None): "Basic constructor." # We don't actually store the parser object: that lets extracted # chunks be garbage-collected self.parserClass = parser.__class__ self.isSelfClosing = parser.isSelfClosingTag(name) self.name = name if attrs is None: attrs = [] elif isinstance(attrs, dict): attrs = attrs.items() self.attrs = attrs self.contents = [] self.setup(parent, previous) self.hidden = False self.containsSubstitutions = False self.convertHTMLEntities = parser.convertHTMLEntities self.convertXMLEntities = parser.convertXMLEntities self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities # Convert any HTML, XML, or numeric entities in the attribute values. convert = lambda(k, val): (k, re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", self._convertEntities, val)) self.attrs = map(convert, self.attrs) def getString(self): if (len(self.contents) == 1 and isinstance(self.contents[0], NavigableString)): return self.contents[0] def setString(self, string): """Replace the contents of the tag with a string""" self.clear() self.append(string) string = property(getString, setString) def getText(self, separator=u""): if not len(self.contents): return u"" stopNode = self._lastRecursiveChild().next strings = [] current = self.contents[0] while current is not stopNode: if isinstance(current, NavigableString): strings.append(current.strip()) current = current.next return separator.join(strings) text = property(getText) def get(self, key, default=None): """Returns the value of the 'key' attribute for the tag, or the value given for 'default' if it doesn't have that attribute.""" return self._getAttrMap().get(key, default) def clear(self): """Extract all children.""" for child in self.contents[:]: child.extract() def index(self, element): for i, child in enumerate(self.contents): if child is element: return i raise ValueError("Tag.index: element not in tag") def has_key(self, key): return self._getAttrMap().has_key(key) def __getitem__(self, key): """tag[key] returns the value of the 'key' attribute for the tag, and throws an exception if it's not there.""" return self._getAttrMap()[key] def __iter__(self): "Iterating over a tag iterates over its contents." return iter(self.contents) def __len__(self): "The length of a tag is the length of its list of contents." return len(self.contents) def __contains__(self, x): return x in self.contents def __nonzero__(self): "A tag is non-None even if it has no contents." return True def __setitem__(self, key, value): """Setting tag[key] sets the value of the 'key' attribute for the tag.""" self._getAttrMap() self.attrMap[key] = value found = False for i in range(0, len(self.attrs)): if self.attrs[i][0] == key: self.attrs[i] = (key, value) found = True if not found: self.attrs.append((key, value)) self._getAttrMap()[key] = value def __delitem__(self, key): "Deleting tag[key] deletes all 'key' attributes for the tag." for item in self.attrs: if item[0] == key: self.attrs.remove(item) #We don't break because bad HTML can define the same #attribute multiple times. self._getAttrMap() if self.attrMap.has_key(key): del self.attrMap[key] def __call__(self, *args, **kwargs): """Calling a tag like a function is the same as calling its findAll() method. Eg. tag('a') returns a list of all the A tags found within this tag.""" return apply(self.findAll, args, kwargs) def __getattr__(self, tag): #print "Getattr %s.%s" % (self.__class__, tag) if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: return self.find(tag[:-3]) elif tag.find('__') != 0: return self.find(tag) raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) def __eq__(self, other): """Returns true iff this tag has the same name, the same attributes, and the same contents (recursively) as the given tag. NOTE: right now this will return false if two tags have the same attributes in a different order. Should this be fixed?""" if other is self: return True if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): return False for i in range(0, len(self.contents)): if self.contents[i] != other.contents[i]: return False return True def __ne__(self, other): """Returns true iff this tag is not identical to the other tag, as defined in __eq__.""" return not self == other def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): """Renders this tag as a string.""" return self.__str__(encoding) def __unicode__(self): return self.__str__(None) BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + ")") def _sub_entity(self, x): """Used with a regular expression to substitute the appropriate XML entity for an XML special character.""" return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, prettyPrint=False, indentLevel=0): """Returns a string or Unicode representation of this tag and its contents. To get Unicode, pass None for encoding. NOTE: since Python's HTML parser consumes whitespace, this method is not certain to reproduce the whitespace present in the original string.""" encodedName = self.toEncoding(self.name, encoding) attrs = [] if self.attrs: for key, val in self.attrs: fmt = '%s="%s"' if isinstance(val, basestring): if self.containsSubstitutions and '%SOUP-ENCODING%' in val: val = self.substituteEncoding(val, encoding) # The attribute value either: # # * Contains no embedded double quotes or single quotes. # No problem: we enclose it in double quotes. # * Contains embedded single quotes. No problem: # double quotes work here too. # * Contains embedded double quotes. No problem: # we enclose it in single quotes. # * Embeds both single _and_ double quotes. This # can't happen naturally, but it can happen if # you modify an attribute value after parsing # the document. Now we have a bit of a # problem. We solve it by enclosing the # attribute in single quotes, and escaping any # embedded single quotes to XML entities. if '"' in val: fmt = "%s='%s'" if "'" in val: # TODO: replace with apos when # appropriate. val = val.replace("'", "&squot;") # Now we're okay w/r/t quotes. But the attribute # value might also contain angle brackets, or # ampersands that aren't part of entities. We need # to escape those to XML entities too. val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) attrs.append(fmt % (self.toEncoding(key, encoding), self.toEncoding(val, encoding))) close = '' closeTag = '' if self.isSelfClosing: close = ' /' else: closeTag = '' % encodedName indentTag, indentContents = 0, 0 if prettyPrint: indentTag = indentLevel space = (' ' * (indentTag-1)) indentContents = indentTag + 1 contents = self.renderContents(encoding, prettyPrint, indentContents) if self.hidden: s = contents else: s = [] attributeString = '' if attrs: attributeString = ' ' + ' '.join(attrs) if prettyPrint: s.append(space) s.append('<%s%s%s>' % (encodedName, attributeString, close)) if prettyPrint: s.append("\n") s.append(contents) if prettyPrint and contents and contents[-1] != "\n": s.append("\n") if prettyPrint and closeTag: s.append(space) s.append(closeTag) if prettyPrint and closeTag and self.nextSibling: s.append("\n") s = ''.join(s) return s def decompose(self): """Recursively destroys the contents of this tree.""" self.extract() if len(self.contents) == 0: return current = self.contents[0] while current is not None: next = current.next if isinstance(current, Tag): del current.contents[:] current.parent = None current.previous = None current.previousSibling = None current.next = None current.nextSibling = None current = next def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): return self.__str__(encoding, True) def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, prettyPrint=False, indentLevel=0): """Renders the contents of this tag as a string in the given encoding. If encoding is None, returns a Unicode string..""" s=[] for c in self: text = None if isinstance(c, NavigableString): text = c.__str__(encoding) elif isinstance(c, Tag): s.append(c.__str__(encoding, prettyPrint, indentLevel)) if text and prettyPrint: text = text.strip() if text: if prettyPrint: s.append(" " * (indentLevel-1)) s.append(text) if prettyPrint: s.append("\n") return ''.join(s) #Soup methods def find(self, name=None, attrs={}, recursive=True, text=None, **kwargs): """Return only the first child of this Tag matching the given criteria.""" r = None l = self.findAll(name, attrs, recursive, text, 1, **kwargs) if l: r = l[0] return r findChild = find def findAll(self, name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs): """Extracts a list of Tag objects that match the given criteria. You can specify the name of the Tag and any attributes you want the Tag to have. The value of a key-value pair in the 'attrs' map can be a string, a list of strings, a regular expression object, or a callable that takes a string and returns whether or not the string matches for some custom definition of 'matches'. The same is true of the tag name.""" generator = self.recursiveChildGenerator if not recursive: generator = self.childGenerator return self._findAll(name, attrs, text, limit, generator, **kwargs) findChildren = findAll # Pre-3.x compatibility methods first = find fetch = findAll def fetchText(self, text=None, recursive=True, limit=None): return self.findAll(text=text, recursive=recursive, limit=limit) def firstText(self, text=None, recursive=True): return self.find(text=text, recursive=recursive) #Private methods def _getAttrMap(self): """Initializes a map representation of this tag's attributes, if not already initialized.""" if not getattr(self, 'attrMap'): self.attrMap = {} for (key, value) in self.attrs: self.attrMap[key] = value return self.attrMap #Generator methods def childGenerator(self): # Just use the iterator from the contents return iter(self.contents) def recursiveChildGenerator(self): if not len(self.contents): raise StopIteration stopNode = self._lastRecursiveChild().next current = self.contents[0] while current is not stopNode: yield current current = current.next # Next, a couple classes to represent queries and their results. class SoupStrainer: """Encapsulates a number of ways of matching a markup element (tag or text).""" def __init__(self, name=None, attrs={}, text=None, **kwargs): self.name = name if isinstance(attrs, basestring): kwargs['class'] = _match_css_class(attrs) attrs = None if kwargs: if attrs: attrs = attrs.copy() attrs.update(kwargs) else: attrs = kwargs self.attrs = attrs self.text = text def __str__(self): if self.text: return self.text else: return "%s|%s" % (self.name, self.attrs) def searchTag(self, markupName=None, markupAttrs={}): found = None markup = None if isinstance(markupName, Tag): markup = markupName markupAttrs = markup callFunctionWithTagData = callable(self.name) \ and not isinstance(markupName, Tag) if (not self.name) \ or callFunctionWithTagData \ or (markup and self._matches(markup, self.name)) \ or (not markup and self._matches(markupName, self.name)): if callFunctionWithTagData: match = self.name(markupName, markupAttrs) else: match = True markupAttrMap = None for attr, matchAgainst in self.attrs.items(): if not markupAttrMap: if hasattr(markupAttrs, 'get'): markupAttrMap = markupAttrs else: markupAttrMap = {} for k,v in markupAttrs: markupAttrMap[k] = v attrValue = markupAttrMap.get(attr) if not self._matches(attrValue, matchAgainst): match = False break if match: if markup: found = markup else: found = markupName return found def search(self, markup): #print 'looking for %s in %s' % (self, markup) found = None # If given a list of items, scan it for a text element that # matches. if hasattr(markup, "__iter__") \ and not isinstance(markup, Tag): for element in markup: if isinstance(element, NavigableString) \ and self.search(element): found = element break # If it's a Tag, make sure its name or attributes match. # Don't bother with Tags if we're searching for text. elif isinstance(markup, Tag): if not self.text: found = self.searchTag(markup) # If it's text, make sure the text matches. elif isinstance(markup, NavigableString) or \ isinstance(markup, basestring): if self._matches(markup, self.text): found = markup else: raise Exception, "I don't know how to match against a %s" \ % markup.__class__ return found def _matches(self, markup, matchAgainst): #print "Matching %s against %s" % (markup, matchAgainst) result = False if matchAgainst is True: result = markup is not None elif callable(matchAgainst): result = matchAgainst(markup) else: #Custom match methods take the tag as an argument, but all #other ways of matching match the tag name as a string. if isinstance(markup, Tag): markup = markup.name if markup and not isinstance(markup, basestring): markup = unicode(markup) #Now we know that chunk is either a string, or None. if hasattr(matchAgainst, 'match'): # It's a regexp object. result = markup and matchAgainst.search(markup) elif hasattr(matchAgainst, '__iter__'): # list-like result = markup in matchAgainst elif hasattr(matchAgainst, 'items'): result = markup.has_key(matchAgainst) elif matchAgainst and isinstance(markup, basestring): if isinstance(markup, unicode): matchAgainst = unicode(matchAgainst) else: matchAgainst = str(matchAgainst) if not result: result = matchAgainst == markup return result class ResultSet(list): """A ResultSet is just a list that keeps track of the SoupStrainer that created it.""" def __init__(self, source): list.__init__([]) self.source = source # Now, some helper functions. def buildTagMap(default, *args): """Turns a list of maps, lists, or scalars into a single map. Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and NESTING_RESET_TAGS maps out of lists and partial maps.""" built = {} for portion in args: if hasattr(portion, 'items'): #It's a map. Merge it. for k,v in portion.items(): built[k] = v elif hasattr(portion, '__iter__'): # is a list #It's a list. Map each item to the default. for k in portion: built[k] = default else: #It's a scalar. Map it to the default. built[portion] = default return built # Now, the parser classes. class BeautifulStoneSoup(Tag, SGMLParser): """This class contains the basic parser and search code. It defines a parser that knows nothing about tag behavior except for the following: You can't close a tag without closing all the tags it encloses. That is, "" actually means "". [Another possible explanation is "", but since this class defines no SELF_CLOSING_TAGS, it will never use that explanation.] This class is useful for parsing XML or made-up markup languages, or when BeautifulSoup makes an assumption counter to what you were expecting.""" SELF_CLOSING_TAGS = {} NESTABLE_TAGS = {} RESET_NESTING_TAGS = {} QUOTE_TAGS = {} PRESERVE_WHITESPACE_TAGS = [] MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), lambda x: x.group(1) + ' />'), (re.compile(']*)>'), lambda x: '') ] ROOT_TAG_NAME = u'[document]' HTML_ENTITIES = "html" XML_ENTITIES = "xml" XHTML_ENTITIES = "xhtml" # TODO: This only exists for backwards-compatibility ALL_ENTITIES = XHTML_ENTITIES # Used when determining whether a text node is all whitespace and # can be replaced with a single space. A text node that contains # fancy Unicode spaces (usually non-breaking) should be left # alone. STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, markupMassage=True, smartQuotesTo=XML_ENTITIES, convertEntities=None, selfClosingTags=None, isHTML=False): """The Soup object is initialized as the 'root tag', and the provided markup (which can be a string or a file-like object) is fed into the underlying parser. sgmllib will process most bad HTML, and the BeautifulSoup class has some tricks for dealing with some HTML that kills sgmllib, but Beautiful Soup can nonetheless choke or lose data if your data uses self-closing tags or declarations incorrectly. By default, Beautiful Soup uses regexes to sanitize input, avoiding the vast majority of these problems. If the problems don't apply to you, pass in False for markupMassage, and you'll get better performance. The default parser massage techniques fix the two most common instances of invalid HTML that choke sgmllib:
(No space between name of closing tag and tag close) (Extraneous whitespace in declaration) You can pass in a custom list of (RE object, replace method) tuples to get Beautiful Soup to scrub your input the way you want.""" self.parseOnlyThese = parseOnlyThese self.fromEncoding = fromEncoding self.smartQuotesTo = smartQuotesTo self.convertEntities = convertEntities # Set the rules for how we'll deal with the entities we # encounter if self.convertEntities: # It doesn't make sense to convert encoded characters to # entities even while you're converting entities to Unicode. # Just convert it all to Unicode. self.smartQuotesTo = None if convertEntities == self.HTML_ENTITIES: self.convertXMLEntities = False self.convertHTMLEntities = True self.escapeUnrecognizedEntities = True elif convertEntities == self.XHTML_ENTITIES: self.convertXMLEntities = True self.convertHTMLEntities = True self.escapeUnrecognizedEntities = False elif convertEntities == self.XML_ENTITIES: self.convertXMLEntities = True self.convertHTMLEntities = False self.escapeUnrecognizedEntities = False else: self.convertXMLEntities = False self.convertHTMLEntities = False self.escapeUnrecognizedEntities = False self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) SGMLParser.__init__(self) if hasattr(markup, 'read'): # It's a file-type object. markup = markup.read() self.markup = markup self.markupMassage = markupMassage try: self._feed(isHTML=isHTML) except StopParsing: pass self.markup = None # The markup can now be GCed def convert_charref(self, name): """This method fixes a bug in Python's SGMLParser.""" try: n = int(name) except ValueError: return if not 0 <= n <= 127 : # ASCII ends at 127, not 255 return return self.convert_codepoint(n) def _feed(self, inDocumentEncoding=None, isHTML=False): # Convert the document to Unicode. markup = self.markup if isinstance(markup, unicode): if not hasattr(self, 'originalEncoding'): self.originalEncoding = None else: dammit = UnicodeDammit\ (markup, [self.fromEncoding, inDocumentEncoding], smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) markup = dammit.unicode self.originalEncoding = dammit.originalEncoding self.declaredHTMLEncoding = dammit.declaredHTMLEncoding if markup: if self.markupMassage: if not hasattr(self.markupMassage, "__iter__"): self.markupMassage = self.MARKUP_MASSAGE for fix, m in self.markupMassage: markup = fix.sub(m, markup) # TODO: We get rid of markupMassage so that the # soup object can be deepcopied later on. Some # Python installations can't copy regexes. If anyone # was relying on the existence of markupMassage, this # might cause problems. del(self.markupMassage) self.reset() SGMLParser.feed(self, markup) # Close out any unfinished strings and close all the open tags. self.endData() while self.currentTag.name != self.ROOT_TAG_NAME: self.popTag() def __getattr__(self, methodName): """This method routes method call requests to either the SGMLParser superclass or the Tag superclass, depending on the method name.""" #print "__getattr__ called on %s.%s" % (self.__class__, methodName) if methodName.startswith('start_') or methodName.startswith('end_') \ or methodName.startswith('do_'): return SGMLParser.__getattr__(self, methodName) elif not methodName.startswith('__'): return Tag.__getattr__(self, methodName) else: raise AttributeError def isSelfClosingTag(self, name): """Returns true iff the given string is the name of a self-closing tag according to this parser.""" return self.SELF_CLOSING_TAGS.has_key(name) \ or self.instanceSelfClosingTags.has_key(name) def reset(self): Tag.__init__(self, self, self.ROOT_TAG_NAME) self.hidden = 1 SGMLParser.reset(self) self.currentData = [] self.currentTag = None self.tagStack = [] self.quoteStack = [] self.pushTag(self) def popTag(self): tag = self.tagStack.pop() #print "Pop", tag.name if self.tagStack: self.currentTag = self.tagStack[-1] return self.currentTag def pushTag(self, tag): #print "Push", tag.name if self.currentTag: self.currentTag.contents.append(tag) self.tagStack.append(tag) self.currentTag = self.tagStack[-1] def endData(self, containerClass=NavigableString): if self.currentData: currentData = u''.join(self.currentData) if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and not set([tag.name for tag in self.tagStack]).intersection( self.PRESERVE_WHITESPACE_TAGS)): if '\n' in currentData: currentData = '\n' else: currentData = ' ' self.currentData = [] if self.parseOnlyThese and len(self.tagStack) <= 1 and \ (not self.parseOnlyThese.text or \ not self.parseOnlyThese.search(currentData)): return o = containerClass(currentData) o.setup(self.currentTag, self.previous) if self.previous: self.previous.next = o self.previous = o self.currentTag.contents.append(o) def _popToTag(self, name, inclusivePop=True): """Pops the tag stack up to and including the most recent instance of the given tag. If inclusivePop is false, pops the tag stack up to but *not* including the most recent instqance of the given tag.""" #print "Popping to %s" % name if name == self.ROOT_TAG_NAME: return numPops = 0 mostRecentTag = None for i in range(len(self.tagStack)-1, 0, -1): if name == self.tagStack[i].name: numPops = len(self.tagStack)-i break if not inclusivePop: numPops = numPops - 1 for i in range(0, numPops): mostRecentTag = self.popTag() return mostRecentTag def _smartPop(self, name): """We need to pop up to the previous tag of this type, unless one of this tag's nesting reset triggers comes between this tag and the previous tag of this type, OR unless this tag is a generic nesting trigger and another generic nesting trigger comes between this tag and the previous tag of this type. Examples:

FooBar *

* should pop to 'p', not 'b'.

FooBar *

* should pop to 'table', not 'p'.

Foo

Bar *

* should pop to 'tr', not 'p'.

    • *
    • * should pop to 'ul', not the first 'li'.
  • ** should pop to 'table', not the first 'tr' tag should implicitly close the previous tag within the same
    ** should pop to 'tr', not the first 'td' """ nestingResetTriggers = self.NESTABLE_TAGS.get(name) isNestable = nestingResetTriggers != None isResetNesting = self.RESET_NESTING_TAGS.has_key(name) popTo = None inclusive = True for i in range(len(self.tagStack)-1, 0, -1): p = self.tagStack[i] if (not p or p.name == name) and not isNestable: #Non-nestable tags get popped to the top or to their #last occurance. popTo = name break if (nestingResetTriggers is not None and p.name in nestingResetTriggers) \ or (nestingResetTriggers is None and isResetNesting and self.RESET_NESTING_TAGS.has_key(p.name)): #If we encounter one of the nesting reset triggers #peculiar to this tag, or we encounter another tag #that causes nesting to reset, pop up to but not #including that tag. popTo = p.name inclusive = False break p = p.parent if popTo: self._popToTag(popTo, inclusive) def unknown_starttag(self, name, attrs, selfClosing=0): #print "Start tag %s: %s" % (name, attrs) if self.quoteStack: #This is not a real tag. #print "<%s> is not real!" % name attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs]) self.handle_data('<%s%s>' % (name, attrs)) return self.endData() if not self.isSelfClosingTag(name) and not selfClosing: self._smartPop(name) if self.parseOnlyThese and len(self.tagStack) <= 1 \ and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): return tag = Tag(self, name, attrs, self.currentTag, self.previous) if self.previous: self.previous.next = tag self.previous = tag self.pushTag(tag) if selfClosing or self.isSelfClosingTag(name): self.popTag() if name in self.QUOTE_TAGS: #print "Beginning quote (%s)" % name self.quoteStack.append(name) self.literal = 1 return tag def unknown_endtag(self, name): #print "End tag %s" % name if self.quoteStack and self.quoteStack[-1] != name: #This is not a real end tag. #print " is not real!" % name self.handle_data('' % name) return self.endData() self._popToTag(name) if self.quoteStack and self.quoteStack[-1] == name: self.quoteStack.pop() self.literal = (len(self.quoteStack) > 0) def handle_data(self, data): self.currentData.append(data) def _toStringSubclass(self, text, subclass): """Adds a certain piece of text to the tree as a NavigableString subclass.""" self.endData() self.handle_data(text) self.endData(subclass) def handle_pi(self, text): """Handle a processing instruction as a ProcessingInstruction object, possibly one with a %SOUP-ENCODING% slot into which an encoding will be plugged later.""" if text[:3] == "xml": text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" self._toStringSubclass(text, ProcessingInstruction) def handle_comment(self, text): "Handle comments as Comment objects." self._toStringSubclass(text, Comment) def handle_charref(self, ref): "Handle character references as data." if self.convertEntities: data = unichr(int(ref)) else: data = '&#%s;' % ref self.handle_data(data) def handle_entityref(self, ref): """Handle entity references as data, possibly converting known HTML and/or XML entity references to the corresponding Unicode characters.""" data = None if self.convertHTMLEntities: try: data = unichr(name2codepoint[ref]) except KeyError: pass if not data and self.convertXMLEntities: data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) if not data and self.convertHTMLEntities and \ not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): # TODO: We've got a problem here. We're told this is # an entity reference, but it's not an XML entity # reference or an HTML entity reference. Nonetheless, # the logical thing to do is to pass it through as an # unrecognized entity reference. # # Except: when the input is "&carol;" this function # will be called with input "carol". When the input is # "AT&T", this function will be called with input # "T". We have no way of knowing whether a semicolon # was present originally, so we don't know whether # this is an unknown entity or just a misplaced # ampersand. # # The more common case is a misplaced ampersand, so I # escape the ampersand and omit the trailing semicolon. data = "&%s" % ref if not data: # This case is different from the one above, because we # haven't already gone through a supposedly comprehensive # mapping of entities to Unicode characters. We might not # have gone through any mapping at all. So the chances are # very high that this is a real entity, and not a # misplaced ampersand. data = "&%s;" % ref self.handle_data(data) def handle_decl(self, data): "Handle DOCTYPEs and the like as Declaration objects." self._toStringSubclass(data, Declaration) def parse_declaration(self, i): """Treat a bogus SGML declaration as raw data. Treat a CDATA declaration as a CData object.""" j = None if self.rawdata[i:i+9] == '', i) if k == -1: k = len(self.rawdata) data = self.rawdata[i+9:k] j = k+3 self._toStringSubclass(data, CData) else: try: j = SGMLParser.parse_declaration(self, i) except SGMLParseError: toHandle = self.rawdata[i:] self.handle_data(toHandle) j = i + len(toHandle) return j class BeautifulSoup(BeautifulStoneSoup): """This parser knows the following facts about HTML: * Some tags have no closing tag and should be interpreted as being closed as soon as they are encountered. * The text inside some tags (ie. 'script') may contain tags which are not really part of the document and which should be parsed as text, not tags. If you want to parse the text as tags, you can always fetch it and parse it explicitly. * Tag nesting rules: Most tags can't be nested at all. For instance, the occurance of a

    tag should implicitly close the previous

    tag.

    Para1

    Para2 should be transformed into:

    Para1

    Para2 Some tags can be nested arbitrarily. For instance, the occurance of a

    tag should _not_ implicitly close the previous
    tag. Alice said:
    Bob said:
    Blah should NOT be transformed into: Alice said:
    Bob said:
    Blah Some tags can be nested, but the nesting is reset by the interposition of other tags. For instance, a
    , but not close a tag in another table.
    BlahBlah should be transformed into:
    BlahBlah but, Blah
    Blah should NOT be transformed into Blah
    Blah Differing assumptions about tag nesting rules are a major source of problems with the BeautifulSoup class. If BeautifulSoup is not treating as nestable a tag your page author treats as nestable, try ICantBelieveItsBeautifulSoup, MinimalSoup, or BeautifulStoneSoup before writing your own subclass.""" def __init__(self, *args, **kwargs): if not kwargs.has_key('smartQuotesTo'): kwargs['smartQuotesTo'] = self.HTML_ENTITIES kwargs['isHTML'] = True BeautifulStoneSoup.__init__(self, *args, **kwargs) SELF_CLOSING_TAGS = buildTagMap(None, ('br' , 'hr', 'input', 'img', 'meta', 'spacer', 'link', 'frame', 'base', 'col')) PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) QUOTE_TAGS = {'script' : None, 'textarea' : None} #According to the HTML standard, each of these inline tags can #contain another tag of the same type. Furthermore, it's common #to actually use these tags this way. NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', 'center') #According to the HTML standard, these block tags can contain #another tag of the same type. Furthermore, it's common #to actually use these tags this way. NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del') #Lists can contain other lists, but there are restrictions. NESTABLE_LIST_TAGS = { 'ol' : [], 'ul' : [], 'li' : ['ul', 'ol'], 'dl' : [], 'dd' : ['dl'], 'dt' : ['dl'] } #Tables can contain other tables, but there are restrictions. NESTABLE_TABLE_TAGS = {'table' : [], 'tr' : ['table', 'tbody', 'tfoot', 'thead'], 'td' : ['tr'], 'th' : ['tr'], 'thead' : ['table'], 'tbody' : ['table'], 'tfoot' : ['table'], } NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre') #If one of these tags is encountered, all tags up to the next tag of #this type are popped. RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', NON_NESTABLE_BLOCK_TAGS, NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) # Used to detect the charset in a META tag; see start_meta CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) def start_meta(self, attrs): """Beautiful Soup can detect a charset included in a META tag, try to convert the document to that charset, and re-parse the document from the beginning.""" httpEquiv = None contentType = None contentTypeIndex = None tagNeedsEncodingSubstitution = False for i in range(0, len(attrs)): key, value = attrs[i] key = key.lower() if key == 'http-equiv': httpEquiv = value elif key == 'content': contentType = value contentTypeIndex = i if httpEquiv and contentType: # It's an interesting meta tag. match = self.CHARSET_RE.search(contentType) if match: if (self.declaredHTMLEncoding is not None or self.originalEncoding == self.fromEncoding): # An HTML encoding was sniffed while converting # the document to Unicode, or an HTML encoding was # sniffed during a previous pass through the # document, or an encoding was specified # explicitly and it worked. Rewrite the meta tag. def rewrite(match): return match.group(1) + "%SOUP-ENCODING%" newAttr = self.CHARSET_RE.sub(rewrite, contentType) attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], newAttr) tagNeedsEncodingSubstitution = True else: # This is our first pass through the document. # Go through it again with the encoding information. newCharset = match.group(3) if newCharset and newCharset != self.originalEncoding: self.declaredHTMLEncoding = newCharset self._feed(self.declaredHTMLEncoding) raise StopParsing pass tag = self.unknown_starttag("meta", attrs) if tag and tagNeedsEncodingSubstitution: tag.containsSubstitutions = True class StopParsing(Exception): pass class ICantBelieveItsBeautifulSoup(BeautifulSoup): """The BeautifulSoup class is oriented towards skipping over common HTML errors like unclosed tags. However, sometimes it makes errors of its own. For instance, consider this fragment: FooBar This is perfectly valid (if bizarre) HTML. However, the BeautifulSoup class will implicitly close the first b tag when it encounters the second 'b'. It will think the author wrote "FooBar", and didn't close the first 'b' tag, because there's no real-world reason to bold something that's already bold. When it encounters '' it will close two more 'b' tags, for a grand total of three tags closed instead of two. This can throw off the rest of your document structure. The same is true of a number of other tags, listed below. It's much more common for someone to forget to close a 'b' tag than to actually use nested 'b' tags, and the BeautifulSoup class handles the common case. This class handles the not-co-common case: where you can't believe someone wrote what they did, but it's valid HTML and BeautifulSoup screwed up by assuming it wouldn't be.""" I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', 'big') I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',) NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) class MinimalSoup(BeautifulSoup): """The MinimalSoup class is for parsing HTML that contains pathologically bad markup. It makes no assumptions about tag nesting, but it does know which tags are self-closing, that